ErisPulse-BilibiliAdapter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,700 @@
1
+ import asyncio
2
+ import json
3
+ import time as _time
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from ErisPulse import sdk
9
+ from ErisPulse.Core.Bases import BaseConfig, BotAccountConfig, BaseAdapter
10
+ try:
11
+ from ErisPulse.runtime.tasks import spawn_background
12
+ except ImportError: # pragma: no cover
13
+ spawn_background = None
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ MIN_FRAMEWORK_VERSION = (2, 7, 1)
18
+
19
+
20
+ def _parse_version(version_str: str):
21
+ try:
22
+ parts = []
23
+ for part in version_str.split("."):
24
+ digits = ""
25
+ for ch in part:
26
+ if ch.isdigit():
27
+ digits += ch
28
+ else:
29
+ break
30
+ parts.append(int(digits or 0))
31
+ while len(parts) < 3:
32
+ parts.append(0)
33
+ return tuple(parts[:3])
34
+ except Exception:
35
+ return (0, 0, 0)
36
+
37
+
38
+ def _check_framework_version() -> bool:
39
+ try:
40
+ raw = sdk.__version__ if hasattr(sdk, "__version__") else sdk.version
41
+ if callable(raw):
42
+ raw = raw()
43
+ current = _parse_version(str(raw))
44
+ if current < MIN_FRAMEWORK_VERSION:
45
+ print(
46
+ f"警告: ErisPulse 版本过低 (当前 {current},需要 >= "
47
+ f"{MIN_FRAMEWORK_VERSION}),BilibiliAdapter 可能无法正常工作"
48
+ )
49
+ return False
50
+ return True
51
+ except Exception:
52
+ return True
53
+
54
+
55
+ from bilibili_api import login_v2, session as bili_session
56
+ from bilibili_api.utils.network import Credential
57
+
58
+
59
+ @dataclass
60
+ class BilibiliAccountConfig(BotAccountConfig):
61
+ """Bilibili 账户配置"""
62
+
63
+ login_mode: str = field(
64
+ default="cookie",
65
+ metadata={
66
+ "description": "登录方式:cookie=手动填写凭据,qrcode=面板扫码登录",
67
+ "required": False,
68
+ "ui": {
69
+ "widget": "select",
70
+ "group": "connection",
71
+ "order": 1,
72
+ "options": [
73
+ {"label": "Cookie 凭据", "value": "cookie"},
74
+ {"label": "扫码登录", "value": "qrcode"},
75
+ ],
76
+ },
77
+ },
78
+ )
79
+ sessdata: str = field(
80
+ default="",
81
+ metadata={
82
+ "description": "SESSDATA(浏览器 Cookie,登录后可见)",
83
+ "required": False,
84
+ "secret": True,
85
+ "ui": {
86
+ "widget": "password",
87
+ "group": "connection",
88
+ "order": 2,
89
+ "show_if": {"field": "login_mode", "op": "eq", "value": "cookie"},
90
+ },
91
+ },
92
+ )
93
+ bili_jct: str = field(
94
+ default="",
95
+ metadata={
96
+ "description": "bili_jct(浏览器 Cookie,csrf 令牌)",
97
+ "required": False,
98
+ "secret": True,
99
+ "ui": {
100
+ "widget": "password",
101
+ "group": "connection",
102
+ "order": 3,
103
+ "show_if": {"field": "login_mode", "op": "eq", "value": "cookie"},
104
+ },
105
+ },
106
+ )
107
+ buvid3: str = field(
108
+ default="",
109
+ metadata={
110
+ "description": "buvid3(浏览器 Cookie,降低风控概率)",
111
+ "required": False,
112
+ "secret": True,
113
+ "ui": {
114
+ "widget": "password",
115
+ "group": "connection",
116
+ "order": 4,
117
+ "show_if": {"field": "login_mode", "op": "eq", "value": "cookie"},
118
+ },
119
+ },
120
+ )
121
+ dedeuserid: str = field(
122
+ default="",
123
+ metadata={
124
+ "description": "DedeUserID(浏览器 Cookie,即账号 UID)",
125
+ "required": False,
126
+ "secret": True,
127
+ "ui": {
128
+ "widget": "password",
129
+ "group": "connection",
130
+ "order": 5,
131
+ "show_if": {"field": "login_mode", "op": "eq", "value": "cookie"},
132
+ },
133
+ },
134
+ )
135
+ bot_id: str = field(
136
+ default="",
137
+ metadata={
138
+ "description": "账号 UID(留空则登录后自动获取)",
139
+ "required": False,
140
+ "ui": {"widget": "text", "group": "basic", "order": 6},
141
+ },
142
+ )
143
+
144
+
145
+ BilibiliAccountConfig._schema_meta = {
146
+ "group_labels": {
147
+ "basic": {"i18n": "bilibili.group.basic", "default": "基本设置"},
148
+ "connection": {"i18n": "bilibili.group.connection", "default": "登录凭据"},
149
+ }
150
+ }
151
+
152
+
153
+ class BilibiliAdapter(BaseAdapter):
154
+ """
155
+ Bilibili 私信适配器
156
+
157
+ 基于 bilibili-api-python,Cookie / 扫码双登录,事件驱动的私信收发。
158
+
159
+ {!--< tips >!--}
160
+ 1. 多账户:accounts 下每个账户对应一个 B 站账号
161
+ 2. 事件:私信消息(detail_type=private),PlatformEvent 提供 bilibili_raw 原始数据
162
+ 3. 发送:支持 text 段与 image 段(image 的 file 为 URL 或本地路径)
163
+ 4. 风控提醒:非官方接口,请控制发送频率,避免账号风控
164
+ {!--< /tips >!--}
165
+ """
166
+
167
+ _platform = "bilibili"
168
+ ConfigClass = None
169
+ AccountConfigClass = BilibiliAccountConfig
170
+
171
+ def __init__(self, sdk_ref=None):
172
+ super().__init__(sdk_ref)
173
+ _check_framework_version()
174
+ self._runtimes: Dict[str, _AccountRuntime] = {}
175
+ self._sessions: Dict[str, Any] = {}
176
+ self._credentials: Dict[str, Credential] = {}
177
+ self._account_tasks: Dict[str, asyncio.Task] = {}
178
+
179
+ def _get_config_key(self) -> str:
180
+ return "BilibiliAdapter"
181
+
182
+ def _build_credential(self, account) -> Optional[Credential]:
183
+ if account.login_mode == "cookie":
184
+ if not (account.sessdata and account.bili_jct):
185
+ return None
186
+ return Credential(
187
+ sessdata=account.sessdata,
188
+ bili_jct=account.bili_jct,
189
+ buvid3=account.buvid3 or None,
190
+ dedeuserid=account.dedeuserid or None,
191
+ )
192
+ # qrcode 模式:凭据由扫码流程写入配置(sessdata 等字段),同样按 cookie 构建
193
+ if account.sessdata and account.bili_jct:
194
+ return Credential(
195
+ sessdata=account.sessdata,
196
+ bili_jct=account.bili_jct,
197
+ buvid3=account.buvid3 or None,
198
+ dedeuserid=account.dedeuserid or None,
199
+ )
200
+ return None
201
+
202
+ async def _detect_bot_id(self, credential: Credential) -> str:
203
+ info = await bili_session.get_self_info(credential)
204
+ return str((info or {}).get("mid", ""))
205
+
206
+ # ==================== 生命周期 ====================
207
+
208
+ async def start(self):
209
+ for name, account in self.enabled_accounts.items():
210
+ credential = self._build_credential(account)
211
+ if credential is None:
212
+ self.logger.warning(
213
+ f"账户 {name} 缺少登录凭据(cookie 模式需 sessdata+bili_jct;"
214
+ "qrcode 模式请在面板扫码登录)"
215
+ )
216
+ continue
217
+
218
+ bot_id = account.bot_id or await self._detect_bot_id(credential)
219
+ self._credentials[name] = credential
220
+
221
+ rt = self._runtimes.get(name)
222
+ if rt is None:
223
+ rt = _AccountRuntime(name, account, bot_id, credential)
224
+ self._runtimes[name] = rt
225
+ rt.config = account
226
+ rt.bot_id = bot_id
227
+ rt.credential = credential
228
+
229
+ task = asyncio.create_task(self._run_account(rt))
230
+ self._account_tasks[name] = task
231
+
232
+ await self.emit_meta("connect", bot_id, user_name=rt.user_name)
233
+
234
+ self.logger.info(
235
+ f"Bilibili 适配器已启动,共 {len(self._account_tasks)} 个账户"
236
+ )
237
+ self._setup_dashboard_section()
238
+
239
+ async def _run_account(self, rt: "_AccountRuntime"):
240
+ """事件驱动接收:Session(credential) + on(TEXT/PICTURE/WITHDRAW) + run()"""
241
+ session = bili_session.Session(rt.credential)
242
+ self._sessions[rt.name] = session
243
+
244
+ @session.on(bili_session.EventType.TEXT)
245
+ async def _on_text(event):
246
+ await self._emit_private_message(rt, event)
247
+
248
+ @session.on(bili_session.EventType.PICTURE)
249
+ async def _on_picture(event):
250
+ await self._emit_private_message(rt, event)
251
+
252
+ @session.on(bili_session.EventType.WITHDRAW)
253
+ async def _on_withdraw(event):
254
+ data = getattr(event, "data", {}) or {}
255
+ self.logger.debug(f"账户 {rt.name} 对方撤回了消息: {data}")
256
+
257
+ try:
258
+ await session.run(exclude_self=True)
259
+ except asyncio.CancelledError:
260
+ raise
261
+ except Exception as e:
262
+ self.logger.error(f"账户 {rt.name} 私信会话异常退出: {e}")
263
+
264
+ async def _emit_private_message(self, rt: "_AccountRuntime", event):
265
+ try:
266
+ data = getattr(event, "data", {}) or {}
267
+ sender_uid = str(data.get("sender_uid") or data.get("sender_id") or "")
268
+ if not sender_uid:
269
+ return
270
+ content = data.get("content") or {}
271
+ if isinstance(content, str):
272
+ try:
273
+ content = json.loads(content)
274
+ except Exception:
275
+ content = {"text": content}
276
+
277
+ raw_type = "text"
278
+ message_segments = []
279
+ alt_parts = []
280
+ if isinstance(content, dict):
281
+ txt = str(content.get("message") or content.get("content") or "")
282
+ if txt:
283
+ message_segments.append({"type": "text", "data": {"text": txt}})
284
+ alt_parts.append(txt)
285
+ for pic in content.get("pictures", []) or []:
286
+ url = pic.get("url") or pic.get("image_url") or ""
287
+ if url:
288
+ message_segments.append({"type": "image", "data": {"file": url}})
289
+ alt_parts.append("[图片]")
290
+ raw_type = "picture"
291
+ if not message_segments:
292
+ return
293
+
294
+ self_info = {
295
+ "platform": "bilibili",
296
+ "user_id": rt.bot_id,
297
+ "user_name": rt.user_name,
298
+ "account_id": rt.name,
299
+ }
300
+ ev = {
301
+ "id": str(uuid.uuid4()),
302
+ "time": int(_time.time()),
303
+ "type": "message",
304
+ "detail_type": "private",
305
+ "platform": "bilibili",
306
+ "self": self_info,
307
+ "user_id": sender_uid,
308
+ "user_nickname": str(content.get("sender_name") or "") or None,
309
+ "message": message_segments,
310
+ "alt_message": " ".join(alt_parts),
311
+ "bilibili_raw": data,
312
+ "bilibili_raw_type": raw_type,
313
+ }
314
+ if ev["user_nickname"] is None:
315
+ ev.pop("user_nickname")
316
+ await self.emit_event(ev)
317
+ except Exception as e:
318
+ self.logger.error(f"处理 B 站私信事件失败: {e}")
319
+
320
+ async def call_api(self, endpoint: str, _account_id: Optional[str] = None, **params):
321
+ """API 动作统一入口(send_message / get_self_info 等)"""
322
+ rt = self._runtimes.get(_account_id) if _account_id else next(iter(self._runtimes.values()), None)
323
+ if rt is None:
324
+ return self.make_error(retcode=10003, message="账户不存在或未启动")
325
+ if endpoint == "send_message":
326
+ target_id = str(params.get("target_id") or "")
327
+ if not target_id:
328
+ return self.make_error(retcode=10003, message="缺少 target_id")
329
+ message = params.get("message") or []
330
+ texts = [str(seg.get("data", {}).get("text", "")) for seg in message if seg.get("type") == "text"]
331
+ pictures = [seg.get("data", {}).get("file") for seg in message if seg.get("type") == "image"]
332
+ result = None
333
+ if texts:
334
+ result = await self._send_text(rt, target_id, "".join(texts))
335
+ for pic in pictures:
336
+ if pic:
337
+ result = await self._send_picture(rt, target_id, pic)
338
+ return result or self.make_error(retcode=10002, message="空消息")
339
+ if endpoint == "get_self_info":
340
+ info = await bili_session.get_self_info(rt.credential)
341
+ return self.make_response(data=info, raw=info)
342
+ return self.make_error(retcode=10002, message=f"不支持的动作: {endpoint}")
343
+
344
+ async def shutdown(self):
345
+ for session in self._sessions.values():
346
+ try:
347
+ await session.close()
348
+ except Exception:
349
+ pass
350
+ self._sessions.clear()
351
+ for task in self._account_tasks.values():
352
+ task.cancel()
353
+ if self._account_tasks:
354
+ await asyncio.gather(*self._account_tasks.values(), return_exceptions=True)
355
+ self._account_tasks.clear()
356
+ for rt in self._runtimes.values():
357
+ if rt.bot_id:
358
+ try:
359
+ await self.emit_meta("disconnect", rt.bot_id)
360
+ except Exception:
361
+ pass
362
+ try:
363
+ sdk.Dashboard.unregister_section("bilibili-qr")
364
+ except Exception:
365
+ pass
366
+ self.logger.info("Bilibili 适配器已关闭")
367
+
368
+ # ==================== 发送 ====================
369
+
370
+ async def _send_text(self, rt: "_AccountRuntime", receiver_id: str, text: str) -> dict:
371
+ result = await bili_session.send_msg(
372
+ rt.credential, int(receiver_id), bili_session.EventType.TEXT, text
373
+ )
374
+ code = (result or {}).get("code", -1)
375
+ if code != 0:
376
+ return self.make_error(
377
+ retcode=33100,
378
+ message=f"B 站接口返回 code={code}: {(result or {}).get('message', '')}",
379
+ raw=result,
380
+ )
381
+ msg_key = ((result or {}).get("data") or {}).get("msg_key", "")
382
+ return self.make_response(data=result, message_id=str(msg_key), raw=result)
383
+
384
+ async def _send_picture(self, rt: "_AccountRuntime", receiver_id: str, file_ref) -> dict:
385
+ from bilibili_api.utils.picture import Picture
386
+
387
+ pic = Picture(url=file_ref) if str(file_ref).startswith(("http://", "https://")) else Picture.from_file(str(file_ref))
388
+ result = await bili_session.send_msg(
389
+ rt.credential, int(receiver_id), bili_session.EventType.PICTURE, pic
390
+ )
391
+ code = (result or {}).get("code", -1)
392
+ if code != 0:
393
+ return self.make_error(retcode=33100, message=f"B 站接口返回 code={code}", raw=result)
394
+ msg_key = ((result or {}).get("data") or {}).get("msg_key", "")
395
+ return self.make_response(data=result, message_id=str(msg_key), raw=result)
396
+
397
+ # ==================== 发送 ====================
398
+
399
+ class Send(BaseAdapter.Send):
400
+ """Bilibili 消息发送(text / image;其余段报错说明)"""
401
+
402
+ def Raw_ob12(self, message, **kwargs):
403
+ ctx = self.send_context
404
+
405
+ async def _send():
406
+ adapter = self._adapter
407
+ rt = adapter._runtimes.get(ctx.get("account_id") or next(iter(adapter._runtimes)))
408
+ if rt is None:
409
+ return adapter.make_error(retcode=10003, message="账户不存在或未启动")
410
+ target_id = ctx.get("target_id")
411
+ if not target_id:
412
+ return adapter.make_error(retcode=10003, message="无法确定发送目标(需要 To('user', <uid>))")
413
+
414
+ texts, pictures, unsupported = [], [], []
415
+ for seg in self._apply_modifiers(message):
416
+ stype = seg.get("type")
417
+ data = seg.get("data", {}) or {}
418
+ if stype == "text":
419
+ texts.append(str(data.get("text", "")))
420
+ elif stype == "image":
421
+ pictures.append(data.get("file"))
422
+ elif stype == "reply":
423
+ continue
424
+ else:
425
+ unsupported.append(stype)
426
+
427
+ result = None
428
+ if texts:
429
+ result = await adapter._send_text(rt, target_id, "".join(texts))
430
+ for pic in pictures:
431
+ result = await adapter._send_picture(rt, target_id, pic)
432
+ if not texts and not pictures:
433
+ return adapter.make_error(
434
+ retcode=10002,
435
+ message="B 站私信仅支持 text/image 段"
436
+ + (f",收到不支持段: {unsupported}" if unsupported else ",且消息为空"),
437
+ )
438
+ return result
439
+
440
+ return asyncio.create_task(_send())
441
+
442
+ # ==================== Api ====================
443
+
444
+ class Api(BaseAdapter.Api):
445
+ """Bilibili 标准 API 动作"""
446
+
447
+ async def get_self_info(self) -> dict:
448
+ rt = self._adapter._current_runtime()
449
+ info = await bili_session.get_self_info(rt.credential)
450
+ data = info or {}
451
+ return self._adapter.make_response(
452
+ data={
453
+ "user_id": str(data.get("mid", "")),
454
+ "user_name": data.get("name", ""),
455
+ "avatar": data.get("face", ""),
456
+ },
457
+ raw=info,
458
+ )
459
+
460
+ async def get_status(self) -> dict:
461
+ return self._adapter.make_response(data={"status": "ok"})
462
+
463
+ async def get_version(self) -> dict:
464
+ from . import __version__
465
+
466
+ return self._adapter.make_response(data={"version": __version__})
467
+
468
+ async def get_supported_actions(self) -> dict:
469
+ return self._adapter.make_response(
470
+ data={
471
+ "actions": [
472
+ "get_self_info", "get_status", "get_version",
473
+ "get_supported_actions", "send_message",
474
+ "get_user_info", "get_session_msgs",
475
+ ]
476
+ }
477
+ )
478
+
479
+ async def get_user_info(self, user_id: str) -> dict:
480
+ """获取用户公开信息(昵称/头像)"""
481
+ from bilibili_api import user as bili_user
482
+
483
+ rt = self._adapter._current_runtime()
484
+ u = bili_user.User(int(user_id), credential=rt.credential)
485
+ info = await u.get_info()
486
+ data = info or {}
487
+ return self._adapter.make_response(
488
+ data={
489
+ "user_id": str(data.get("mid", user_id)),
490
+ "user_name": data.get("name", ""),
491
+ "avatar": data.get("face", ""),
492
+ },
493
+ raw=info,
494
+ )
495
+
496
+ async def get_session_msgs(self, talker_id: str, begin_seqno: int = 0) -> dict:
497
+ """拉取指定会话的历史消息"""
498
+ rt = self._adapter._current_runtime()
499
+ result = await bili_session.fetch_session_msgs(
500
+ int(talker_id), rt.credential, begin_seqno=begin_seqno
501
+ )
502
+ return self._adapter.make_response(data=result, raw=result)
503
+
504
+ def _current_runtime(self):
505
+ name = getattr(self, "_api_account_id", None)
506
+ if name and name in self._runtimes:
507
+ return self._runtimes[name]
508
+ return next(iter(self._runtimes.values()))
509
+
510
+ # ==================== 扫码登录(面板扩展区) ====================
511
+
512
+ def _setup_dashboard_section(self):
513
+ def _register():
514
+ try:
515
+ dashboard = sdk.Dashboard
516
+ except Exception:
517
+ return False
518
+ try:
519
+ dashboard.register_section(
520
+ id="bilibili-qr",
521
+ target="account_wizard",
522
+ title="扫码登录 B 站",
523
+ title_en="Scan QR to log in",
524
+ icon_svg='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM21 14v.01M14 21v.01M17.5 17.5L21 21"/></svg>',
525
+ platforms=["bilibili"],
526
+ html=self._qr_section_html(),
527
+ order=10,
528
+ )
529
+ self.logger.info("已注册面板扫码登录扩展区")
530
+ return True
531
+ except Exception as e:
532
+ self.logger.debug(f"面板扩展区注册失败: {e}")
533
+ return False
534
+
535
+ if _register():
536
+ return
537
+
538
+ async def _on_module_load(data):
539
+ if (
540
+ isinstance(data, dict)
541
+ and data.get("module_name") == "Dashboard"
542
+ and data.get("success")
543
+ ):
544
+ _register()
545
+ sdk.lifecycle.unregister("module.load", _on_module_load)
546
+
547
+ try:
548
+ sdk.lifecycle.register("module.load", _on_module_load)
549
+ except Exception:
550
+ pass
551
+
552
+ def _ensure_qr_routes(self):
553
+ """扫码端点注册(幂等)"""
554
+ if getattr(self, "_qr_routes_ready", False):
555
+ return
556
+ from ErisPulse.Core import router
557
+
558
+ self._qr_sessions: Dict[str, Any] = {}
559
+
560
+ async def _qr_start(request):
561
+ from fastapi.responses import JSONResponse
562
+
563
+ qrl = login_v2.QrCodeLogin(login_v2.QrCodeLoginChannel.WEB)
564
+ await qrl.generate_qrcode()
565
+ pic = qrl.get_qrcode_picture()
566
+ import secrets as _secrets
567
+
568
+ poll_token = _secrets.token_urlsafe(24)
569
+ task_id = uuid.uuid4().hex
570
+ self._qr_sessions[task_id] = {"qrl": qrl, "token": poll_token}
571
+ return JSONResponse(
572
+ {
573
+ "success": True,
574
+ "task_id": task_id,
575
+ "poll_token": poll_token,
576
+ "qr_url": pic.url or "",
577
+ }
578
+ )
579
+
580
+ async def _qr_poll(request):
581
+ from fastapi.responses import JSONResponse
582
+
583
+ body = {}
584
+ try:
585
+ body = await request.json()
586
+ except Exception:
587
+ pass
588
+ task_id = str(body.get("task_id") or "")
589
+ token = str(body.get("poll_token") or "")
590
+ entry = self._qr_sessions.get(task_id)
591
+ if not entry or entry["token"] != token:
592
+ return JSONResponse({"success": False, "error": "unauthorized"}, status_code=403)
593
+ qrl = entry["qrl"]
594
+ try:
595
+ state = await qrl.check_state()
596
+ except Exception as e:
597
+ return JSONResponse({"success": False, "error": str(e)}, status_code=500)
598
+ out = {"success": True, "status": str(state.value) if hasattr(state, "value") else str(state)}
599
+ if state == login_v2.QrCodeLoginEvents.DONE:
600
+ cred = await qrl.get_credential()
601
+ from ErisPulse.Core import config as _cfg
602
+
603
+ key = "BilibiliAdapter.accounts"
604
+ data = _cfg.getConfig(key) or {}
605
+ if not isinstance(data, dict):
606
+ data = {}
607
+ dft = data.get("default")
608
+ if isinstance(dft, dict) and not dft.get("sessdata"):
609
+ name = "default"
610
+ else:
611
+ base, i, name = "qr", 1, "qr"
612
+ while name in data and data[name].get("sessdata"):
613
+ i += 1
614
+ name = f"{base}{i}"
615
+ data[name] = {
616
+ "login_mode": "qrcode",
617
+ "sessdata": cred.sessdata or "",
618
+ "bili_jct": cred.bili_jct or "",
619
+ "buvid3": cred.buvid3 or "",
620
+ "dedeuserid": cred.dedeuserid or "",
621
+ "bot_id": cred.dedeuserid or "",
622
+ "enabled": True,
623
+ }
624
+ _cfg.setConfig(key, data)
625
+ self._qr_sessions.pop(task_id, None)
626
+ out["account"] = name
627
+ self.logger.info(f"扫码登录完成,凭据已写入账户 {name}")
628
+ elif state == login_v2.QrCodeLoginEvents.TIMEOUT:
629
+ self._qr_sessions.pop(task_id, None)
630
+ return JSONResponse(out)
631
+
632
+ router.register_http_route("bilibili", "/qr/start", _qr_start, methods=["POST"])
633
+ router.register_http_route("bilibili", "/qr/poll", _qr_poll, methods=["POST"])
634
+ self._qr_routes_ready = True
635
+ self.logger.debug("扫码登录端点已注册 (/bilibili/qr/*)")
636
+
637
+ @staticmethod
638
+ def _qr_section_html() -> str:
639
+ return '''<!DOCTYPE html>
640
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
641
+ <style>
642
+ :root{color-scheme:light dark}
643
+ body{margin:0;padding:18px;font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:transparent;color:inherit;display:flex;flex-direction:column;align-items:center;gap:12px}
644
+ #qr{border-radius:12px;overflow:hidden;box-shadow:0 0 0 1px rgba(128,128,128,.25);background:#fff;padding:8px;min-width:200px;min-height:200px;display:flex;align-items:center;justify-content:center}
645
+ #qr img{width:200px;height:200px;image-rendering:pixelated}
646
+ .st{font-size:13px;opacity:.75;text-align:center}
647
+ .ok{color:#16a34a;font-weight:600}
648
+ button{padding:8px 18px;border-radius:10px;border:1px solid rgba(128,128,128,.4);background:transparent;cursor:pointer;font-size:13px}
649
+ </style></head><body>
650
+ <div id="qr">…</div>
651
+ <div class="st" id="st">正在获取二维码…</div>
652
+ <button id="re" style="display:none" onclick="location.reload()">重新获取</button>
653
+ <script>
654
+ (async function(){
655
+ try{
656
+ var r = await fetch("/bilibili/qr/start", {method:"POST"});
657
+ var d = await r.json();
658
+ if(!d.success) throw new Error(d.error || "start failed");
659
+ document.getElementById("qr").innerHTML = '<img src="' + d.qr_url + '" alt="QR">';
660
+ document.getElementById("st").textContent = "请使用 B 站 APP 扫码并确认登录";
661
+ var taskId = d.task_id, token = d.poll_token, n = 0;
662
+ var timer = setInterval(async function(){
663
+ n++;
664
+ if(n > 90){ clearInterval(timer); document.getElementById("st").textContent="二维码已过期"; document.getElementById("re").style.display=""; return; }
665
+ try{
666
+ var pr = await fetch("/bilibili/qr/poll", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({task_id:taskId, poll_token:token})});
667
+ var pd = await pr.json();
668
+ if(pd.success && pd.status === "done"){
669
+ clearInterval(timer);
670
+ document.getElementById("st").className = "st ok";
671
+ document.getElementById("st").textContent = "登录成功,凭据已写入账户 " + (pd.account || "");
672
+ setTimeout(function(){ parent.postMessage({type:"ep_section_done", success:true}, "*"); }, 900);
673
+ } else if(pd.success && pd.status === "timeout"){
674
+ clearInterval(timer);
675
+ document.getElementById("st").textContent = "二维码已过期";
676
+ document.getElementById("re").style.display="";
677
+ } else if(pd.success && pd.status === "scan"){
678
+ document.getElementById("st").textContent = "已扫码,请在手机上确认";
679
+ }
680
+ }catch(e){}
681
+ }, 2000);
682
+ }catch(e){
683
+ document.getElementById("st").textContent = "获取二维码失败: " + e.message;
684
+ document.getElementById("re").style.display="";
685
+ }
686
+ })();
687
+ </script></body></html>'''
688
+
689
+
690
+ class _AccountRuntime:
691
+ """单个账户运行时状态"""
692
+
693
+ __slots__ = ("name", "config", "bot_id", "credential", "user_name")
694
+
695
+ def __init__(self, name: str, config, bot_id: str, credential):
696
+ self.name = name
697
+ self.config = config
698
+ self.bot_id = bot_id
699
+ self.credential = credential
700
+ self.user_name = ""
@@ -0,0 +1,3 @@
1
+ from .Core import BilibiliAdapter, __version__
2
+
3
+ __all__ = ["BilibiliAdapter", "__version__"]
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-BilibiliAdapter
3
+ Version: 0.1.0
4
+ Summary: ErisPulse Bilibili 私信适配器 — Cookie/扫码双登录、事件驱动收发、多账户
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: ErisPulse>=2.7.1
10
+ Requires-Dist: bilibili-api-python>=16.3.0
11
+ Dynamic: license-file
12
+
13
+ [English](README.en.md) | [简体中文](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [Русский](README.ru.md)
14
+
15
+ <div align="center">
16
+
17
+ <img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" width="180" alt="ErisPulse-BilibiliAdapter" />
18
+
19
+ # ErisPulse-BilibiliAdapter
20
+
21
+ **ErisPulse Bilibili DM adapter — QR/Cookie login, event-driven DMs, multi-account**
22
+
23
+ <p>
24
+ <a href="https://pypi.org/project/ErisPulse-BilibiliAdapter/"><img src="https://img.shields.io/pypi/v/ErisPulse-BilibiliAdapter?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
25
+ <a href="https://pypi.org/project/ErisPulse-BilibiliAdapter/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
26
+ <a href="https://github.com/ErisPulse/ErisPulse-BilibiliAdapter/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
27
+ <img src="https://raw.githubusercontent.com/ErisPulse/.github/main/assets/badges/adapter.svg" alt="ErisPulse">
28
+ </p>
29
+
30
+ </div>
31
+
32
+ ---
33
+
34
+ ## Introduction
35
+
36
+ BilibiliAdapter is a Bilibili direct-message adapter built on [bilibili-api-python](https://github.com/Passkou/bilibili-api), providing event-driven DM send/receive.
37
+
38
+ **Highlights:**
39
+ - **Dual login**: Dashboard QR-code login (credentials written to config automatically) / manual Cookie credentials
40
+ - **Event-driven DMs**: text / picture messages received in real time as standard OneBot12 events
41
+ - **Picture in/out**: inbound as image segment (URL); outbound supports URL / local path
42
+ - **Multi-account**: one Bilibili account per entry under `accounts`, independent session listeners
43
+ - **Safe QR flow**: QR session tokens prevent unauthorized polling; credentials shown as passwords in the panel
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ epsdk install BilibiliAdapter
49
+ ```
50
+
51
+ или:
52
+
53
+ ```bash
54
+ pip install ErisPulse-BilibiliAdapter
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ **Option 1: QR-code login (recommended)**
60
+
61
+ ```toml
62
+ [BilibiliAdapter.accounts.default]
63
+ login_mode = "qrcode"
64
+ bot_id = "" # auto-detect
65
+ enabled = true
66
+ ```
67
+
68
+ > After saving, open **Dashboard → Component Config → Bilibili → Add account wizard**, choose "Scan QR to log in", scan with the Bilibili APP and credentials are written to config automatically.
69
+
70
+ **Option 2: Cookie credentials**
71
+
72
+ ```toml
73
+ [BilibiliAdapter.accounts.default]
74
+ login_mode = "cookie"
75
+ sessdata = "YOUR_SESSDATA" # required
76
+ bili_jct = "YOUR_BILI_JCT" # required
77
+ buvid3 = "YOUR_BUVID3" # optional
78
+ dedeuserid = "YOUR_UID" # optional
79
+ bot_id = "" # auto-detect
80
+ enabled = true
81
+ ```
82
+
83
+ > To get cookies: log in to Bilibili in your browser → F12 devtools → Application → Cookies → copy the fields.
84
+
85
+ **Configuration notes (per account):**
86
+ - `login_mode`: `cookie` (manual credentials) or `qrcode` (panel scan, recommended)
87
+ - `sessdata` / `bili_jct`: login cookies (required for cookie mode, shown as passwords)
88
+ - `buvid3`: device fingerprint cookie (optional, reduces risk-control probability)
89
+ - `dedeuserid`: account UID (optional, default source for bot_id)
90
+ - `bot_id`: account UID (auto-detected after login when empty)
91
+
92
+ ## Messaging examples
93
+
94
+ ```python
95
+ from ErisPulse import sdk
96
+ bili = sdk.adapter.get("bilibili")
97
+
98
+ # Send a text DM (target is the peer UID)
99
+ await bili.Send.To("user", "123456789").Text("Hello Bilibili!")
100
+
101
+ # Send a picture (URL or local path)
102
+ await bili.Send.To("user", "123456789").Image("https://example.com/pic.jpg")
103
+
104
+ # Reply (chained modifier)
105
+ await bili.Send.To("user", "123456789").Reply("msg_key").Text("Got it!")
106
+
107
+ # Multi-account
108
+ await bili.Send.Using("account2").To("user", "123456789").Text("From the second account")
109
+
110
+ # OneBot12 standard segments
111
+ message = [
112
+ {"type": "text", "data": {"text": "line one"}},
113
+ {"type": "image", "data": {"file": "https://example.com/img.jpg"}},
114
+ ]
115
+ await bili.Send.To("user", "123456789").Raw_ob12(message)
116
+ ```
117
+
118
+ ## Standard Api actions
119
+
120
+ ```python
121
+ # Self info
122
+ result = await bili.Api.get_self_info() # {user_id, user_name, avatar}
123
+
124
+ # Public user info (nickname / avatar)
125
+ result = await bili.Api.get_user_info("123456789")
126
+
127
+ # Fetch history messages of a session
128
+ result = await bili.Api.get_session_msgs("123456789", begin_seqno=0)
129
+
130
+ # Status & version
131
+ result = await bili.Api.get_status()
132
+ result = await bili.Api.get_version()
133
+
134
+ # Multi-account
135
+ result = await bili.Api.Using("account2").get_self_info()
136
+ ```
137
+
138
+ ## Event listening examples
139
+
140
+ ### Using the Event module (recommended)
141
+
142
+ ```python
143
+ @sdk.adapter.on_message()
144
+ async def on_message(event):
145
+ if event["platform"] == "bilibili":
146
+ sender = event["user_id"] # peer UID
147
+ text = event["alt_message"] # plain text content
148
+ await event.reply("Got it!") # quick reply
149
+ ```
150
+
151
+ ### Using platform raw events
152
+
153
+ ```python
154
+ @sdk.adapter.on("message")
155
+ async def handle_message(event):
156
+ if event["platform"] == "bilibili":
157
+ raw = event["bilibili_raw"] # raw Bilibili DM payload
158
+ print(f"DM from: {raw.get('sender_uid')}")
159
+ ```
160
+
161
+ ## Platform event mapping
162
+
163
+ | Bilibili DM event | Adapter event |
164
+ |-------------------|---------------|
165
+ | TEXT (text message) | message (text segment) |
166
+ | PICTURE (picture message) | message (image segment) |
167
+ | WITHDRAW (recall) | logged only |
168
+
169
+ ## Important notes:
170
+ 1. **Bilibili has no official personal Bot API** — this adapter uses the web DM interface via simulated login (unofficial)
171
+ 2. **Throttle your sends**: high-frequency / bulk DMs easily trigger risk control (DM功能 temporary ban); add rate limiting on the module side
172
+ 3. Do not use for harassment or bulk marketing; follow Bilibili community rules
173
+ 4. Cookies equal full account access — keep them safe; credential fields are masked in the panel
174
+ 5. Only direct chats are supported; fan-group chats are not
175
+ 6. Inbound pictures become image segments (URL); outbound pictures accept URL / local path
176
+ 7. When the login state expires (cookies stale) the adapter logs errors — re-scan or update cookies
177
+
178
+ ---
179
+
180
+ ### Reference links
181
+ - [ErisPulse main repository](https://github.com/ErisPulse/ErisPulse/)
182
+ - [bilibili-api-python](https://github.com/Passkou/bilibili-api)
183
+ - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
184
+ - [Module development guide](https://www.erisdev.com/#docs/developer-guide/README.md)
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ BilibiliAdapter/Core.py
5
+ BilibiliAdapter/__init__.py
6
+ ErisPulse_BilibiliAdapter.egg-info/PKG-INFO
7
+ ErisPulse_BilibiliAdapter.egg-info/SOURCES.txt
8
+ ErisPulse_BilibiliAdapter.egg-info/dependency_links.txt
9
+ ErisPulse_BilibiliAdapter.egg-info/entry_points.txt
10
+ ErisPulse_BilibiliAdapter.egg-info/requires.txt
11
+ ErisPulse_BilibiliAdapter.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [erispulse.adapter]
2
+ bilibili = BilibiliAdapter:BilibiliAdapter
@@ -0,0 +1,2 @@
1
+ ErisPulse>=2.7.1
2
+ bilibili-api-python>=16.3.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ErisPulse
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-BilibiliAdapter
3
+ Version: 0.1.0
4
+ Summary: ErisPulse Bilibili 私信适配器 — Cookie/扫码双登录、事件驱动收发、多账户
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: ErisPulse>=2.7.1
10
+ Requires-Dist: bilibili-api-python>=16.3.0
11
+ Dynamic: license-file
12
+
13
+ [English](README.en.md) | [简体中文](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [Русский](README.ru.md)
14
+
15
+ <div align="center">
16
+
17
+ <img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" width="180" alt="ErisPulse-BilibiliAdapter" />
18
+
19
+ # ErisPulse-BilibiliAdapter
20
+
21
+ **ErisPulse Bilibili DM adapter — QR/Cookie login, event-driven DMs, multi-account**
22
+
23
+ <p>
24
+ <a href="https://pypi.org/project/ErisPulse-BilibiliAdapter/"><img src="https://img.shields.io/pypi/v/ErisPulse-BilibiliAdapter?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
25
+ <a href="https://pypi.org/project/ErisPulse-BilibiliAdapter/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
26
+ <a href="https://github.com/ErisPulse/ErisPulse-BilibiliAdapter/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
27
+ <img src="https://raw.githubusercontent.com/ErisPulse/.github/main/assets/badges/adapter.svg" alt="ErisPulse">
28
+ </p>
29
+
30
+ </div>
31
+
32
+ ---
33
+
34
+ ## Introduction
35
+
36
+ BilibiliAdapter is a Bilibili direct-message adapter built on [bilibili-api-python](https://github.com/Passkou/bilibili-api), providing event-driven DM send/receive.
37
+
38
+ **Highlights:**
39
+ - **Dual login**: Dashboard QR-code login (credentials written to config automatically) / manual Cookie credentials
40
+ - **Event-driven DMs**: text / picture messages received in real time as standard OneBot12 events
41
+ - **Picture in/out**: inbound as image segment (URL); outbound supports URL / local path
42
+ - **Multi-account**: one Bilibili account per entry under `accounts`, independent session listeners
43
+ - **Safe QR flow**: QR session tokens prevent unauthorized polling; credentials shown as passwords in the panel
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ epsdk install BilibiliAdapter
49
+ ```
50
+
51
+ или:
52
+
53
+ ```bash
54
+ pip install ErisPulse-BilibiliAdapter
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ **Option 1: QR-code login (recommended)**
60
+
61
+ ```toml
62
+ [BilibiliAdapter.accounts.default]
63
+ login_mode = "qrcode"
64
+ bot_id = "" # auto-detect
65
+ enabled = true
66
+ ```
67
+
68
+ > After saving, open **Dashboard → Component Config → Bilibili → Add account wizard**, choose "Scan QR to log in", scan with the Bilibili APP and credentials are written to config automatically.
69
+
70
+ **Option 2: Cookie credentials**
71
+
72
+ ```toml
73
+ [BilibiliAdapter.accounts.default]
74
+ login_mode = "cookie"
75
+ sessdata = "YOUR_SESSDATA" # required
76
+ bili_jct = "YOUR_BILI_JCT" # required
77
+ buvid3 = "YOUR_BUVID3" # optional
78
+ dedeuserid = "YOUR_UID" # optional
79
+ bot_id = "" # auto-detect
80
+ enabled = true
81
+ ```
82
+
83
+ > To get cookies: log in to Bilibili in your browser → F12 devtools → Application → Cookies → copy the fields.
84
+
85
+ **Configuration notes (per account):**
86
+ - `login_mode`: `cookie` (manual credentials) or `qrcode` (panel scan, recommended)
87
+ - `sessdata` / `bili_jct`: login cookies (required for cookie mode, shown as passwords)
88
+ - `buvid3`: device fingerprint cookie (optional, reduces risk-control probability)
89
+ - `dedeuserid`: account UID (optional, default source for bot_id)
90
+ - `bot_id`: account UID (auto-detected after login when empty)
91
+
92
+ ## Messaging examples
93
+
94
+ ```python
95
+ from ErisPulse import sdk
96
+ bili = sdk.adapter.get("bilibili")
97
+
98
+ # Send a text DM (target is the peer UID)
99
+ await bili.Send.To("user", "123456789").Text("Hello Bilibili!")
100
+
101
+ # Send a picture (URL or local path)
102
+ await bili.Send.To("user", "123456789").Image("https://example.com/pic.jpg")
103
+
104
+ # Reply (chained modifier)
105
+ await bili.Send.To("user", "123456789").Reply("msg_key").Text("Got it!")
106
+
107
+ # Multi-account
108
+ await bili.Send.Using("account2").To("user", "123456789").Text("From the second account")
109
+
110
+ # OneBot12 standard segments
111
+ message = [
112
+ {"type": "text", "data": {"text": "line one"}},
113
+ {"type": "image", "data": {"file": "https://example.com/img.jpg"}},
114
+ ]
115
+ await bili.Send.To("user", "123456789").Raw_ob12(message)
116
+ ```
117
+
118
+ ## Standard Api actions
119
+
120
+ ```python
121
+ # Self info
122
+ result = await bili.Api.get_self_info() # {user_id, user_name, avatar}
123
+
124
+ # Public user info (nickname / avatar)
125
+ result = await bili.Api.get_user_info("123456789")
126
+
127
+ # Fetch history messages of a session
128
+ result = await bili.Api.get_session_msgs("123456789", begin_seqno=0)
129
+
130
+ # Status & version
131
+ result = await bili.Api.get_status()
132
+ result = await bili.Api.get_version()
133
+
134
+ # Multi-account
135
+ result = await bili.Api.Using("account2").get_self_info()
136
+ ```
137
+
138
+ ## Event listening examples
139
+
140
+ ### Using the Event module (recommended)
141
+
142
+ ```python
143
+ @sdk.adapter.on_message()
144
+ async def on_message(event):
145
+ if event["platform"] == "bilibili":
146
+ sender = event["user_id"] # peer UID
147
+ text = event["alt_message"] # plain text content
148
+ await event.reply("Got it!") # quick reply
149
+ ```
150
+
151
+ ### Using platform raw events
152
+
153
+ ```python
154
+ @sdk.adapter.on("message")
155
+ async def handle_message(event):
156
+ if event["platform"] == "bilibili":
157
+ raw = event["bilibili_raw"] # raw Bilibili DM payload
158
+ print(f"DM from: {raw.get('sender_uid')}")
159
+ ```
160
+
161
+ ## Platform event mapping
162
+
163
+ | Bilibili DM event | Adapter event |
164
+ |-------------------|---------------|
165
+ | TEXT (text message) | message (text segment) |
166
+ | PICTURE (picture message) | message (image segment) |
167
+ | WITHDRAW (recall) | logged only |
168
+
169
+ ## Important notes:
170
+ 1. **Bilibili has no official personal Bot API** — this adapter uses the web DM interface via simulated login (unofficial)
171
+ 2. **Throttle your sends**: high-frequency / bulk DMs easily trigger risk control (DM功能 temporary ban); add rate limiting on the module side
172
+ 3. Do not use for harassment or bulk marketing; follow Bilibili community rules
173
+ 4. Cookies equal full account access — keep them safe; credential fields are masked in the panel
174
+ 5. Only direct chats are supported; fan-group chats are not
175
+ 6. Inbound pictures become image segments (URL); outbound pictures accept URL / local path
176
+ 7. When the login state expires (cookies stale) the adapter logs errors — re-scan or update cookies
177
+
178
+ ---
179
+
180
+ ### Reference links
181
+ - [ErisPulse main repository](https://github.com/ErisPulse/ErisPulse/)
182
+ - [bilibili-api-python](https://github.com/Passkou/bilibili-api)
183
+ - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
184
+ - [Module development guide](https://www.erisdev.com/#docs/developer-guide/README.md)
@@ -0,0 +1,172 @@
1
+ [English](README.en.md) | [简体中文](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [Русский](README.ru.md)
2
+
3
+ <div align="center">
4
+
5
+ <img src="https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/.github/assets/ErisPulseLogo.png" width="180" alt="ErisPulse-BilibiliAdapter" />
6
+
7
+ # ErisPulse-BilibiliAdapter
8
+
9
+ **ErisPulse Bilibili DM adapter — QR/Cookie login, event-driven DMs, multi-account**
10
+
11
+ <p>
12
+ <a href="https://pypi.org/project/ErisPulse-BilibiliAdapter/"><img src="https://img.shields.io/pypi/v/ErisPulse-BilibiliAdapter?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
13
+ <a href="https://pypi.org/project/ErisPulse-BilibiliAdapter/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
14
+ <a href="https://github.com/ErisPulse/ErisPulse-BilibiliAdapter/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
15
+ <img src="https://raw.githubusercontent.com/ErisPulse/.github/main/assets/badges/adapter.svg" alt="ErisPulse">
16
+ </p>
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Introduction
23
+
24
+ BilibiliAdapter is a Bilibili direct-message adapter built on [bilibili-api-python](https://github.com/Passkou/bilibili-api), providing event-driven DM send/receive.
25
+
26
+ **Highlights:**
27
+ - **Dual login**: Dashboard QR-code login (credentials written to config automatically) / manual Cookie credentials
28
+ - **Event-driven DMs**: text / picture messages received in real time as standard OneBot12 events
29
+ - **Picture in/out**: inbound as image segment (URL); outbound supports URL / local path
30
+ - **Multi-account**: one Bilibili account per entry under `accounts`, independent session listeners
31
+ - **Safe QR flow**: QR session tokens prevent unauthorized polling; credentials shown as passwords in the panel
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ epsdk install BilibiliAdapter
37
+ ```
38
+
39
+ или:
40
+
41
+ ```bash
42
+ pip install ErisPulse-BilibiliAdapter
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ **Option 1: QR-code login (recommended)**
48
+
49
+ ```toml
50
+ [BilibiliAdapter.accounts.default]
51
+ login_mode = "qrcode"
52
+ bot_id = "" # auto-detect
53
+ enabled = true
54
+ ```
55
+
56
+ > After saving, open **Dashboard → Component Config → Bilibili → Add account wizard**, choose "Scan QR to log in", scan with the Bilibili APP and credentials are written to config automatically.
57
+
58
+ **Option 2: Cookie credentials**
59
+
60
+ ```toml
61
+ [BilibiliAdapter.accounts.default]
62
+ login_mode = "cookie"
63
+ sessdata = "YOUR_SESSDATA" # required
64
+ bili_jct = "YOUR_BILI_JCT" # required
65
+ buvid3 = "YOUR_BUVID3" # optional
66
+ dedeuserid = "YOUR_UID" # optional
67
+ bot_id = "" # auto-detect
68
+ enabled = true
69
+ ```
70
+
71
+ > To get cookies: log in to Bilibili in your browser → F12 devtools → Application → Cookies → copy the fields.
72
+
73
+ **Configuration notes (per account):**
74
+ - `login_mode`: `cookie` (manual credentials) or `qrcode` (panel scan, recommended)
75
+ - `sessdata` / `bili_jct`: login cookies (required for cookie mode, shown as passwords)
76
+ - `buvid3`: device fingerprint cookie (optional, reduces risk-control probability)
77
+ - `dedeuserid`: account UID (optional, default source for bot_id)
78
+ - `bot_id`: account UID (auto-detected after login when empty)
79
+
80
+ ## Messaging examples
81
+
82
+ ```python
83
+ from ErisPulse import sdk
84
+ bili = sdk.adapter.get("bilibili")
85
+
86
+ # Send a text DM (target is the peer UID)
87
+ await bili.Send.To("user", "123456789").Text("Hello Bilibili!")
88
+
89
+ # Send a picture (URL or local path)
90
+ await bili.Send.To("user", "123456789").Image("https://example.com/pic.jpg")
91
+
92
+ # Reply (chained modifier)
93
+ await bili.Send.To("user", "123456789").Reply("msg_key").Text("Got it!")
94
+
95
+ # Multi-account
96
+ await bili.Send.Using("account2").To("user", "123456789").Text("From the second account")
97
+
98
+ # OneBot12 standard segments
99
+ message = [
100
+ {"type": "text", "data": {"text": "line one"}},
101
+ {"type": "image", "data": {"file": "https://example.com/img.jpg"}},
102
+ ]
103
+ await bili.Send.To("user", "123456789").Raw_ob12(message)
104
+ ```
105
+
106
+ ## Standard Api actions
107
+
108
+ ```python
109
+ # Self info
110
+ result = await bili.Api.get_self_info() # {user_id, user_name, avatar}
111
+
112
+ # Public user info (nickname / avatar)
113
+ result = await bili.Api.get_user_info("123456789")
114
+
115
+ # Fetch history messages of a session
116
+ result = await bili.Api.get_session_msgs("123456789", begin_seqno=0)
117
+
118
+ # Status & version
119
+ result = await bili.Api.get_status()
120
+ result = await bili.Api.get_version()
121
+
122
+ # Multi-account
123
+ result = await bili.Api.Using("account2").get_self_info()
124
+ ```
125
+
126
+ ## Event listening examples
127
+
128
+ ### Using the Event module (recommended)
129
+
130
+ ```python
131
+ @sdk.adapter.on_message()
132
+ async def on_message(event):
133
+ if event["platform"] == "bilibili":
134
+ sender = event["user_id"] # peer UID
135
+ text = event["alt_message"] # plain text content
136
+ await event.reply("Got it!") # quick reply
137
+ ```
138
+
139
+ ### Using platform raw events
140
+
141
+ ```python
142
+ @sdk.adapter.on("message")
143
+ async def handle_message(event):
144
+ if event["platform"] == "bilibili":
145
+ raw = event["bilibili_raw"] # raw Bilibili DM payload
146
+ print(f"DM from: {raw.get('sender_uid')}")
147
+ ```
148
+
149
+ ## Platform event mapping
150
+
151
+ | Bilibili DM event | Adapter event |
152
+ |-------------------|---------------|
153
+ | TEXT (text message) | message (text segment) |
154
+ | PICTURE (picture message) | message (image segment) |
155
+ | WITHDRAW (recall) | logged only |
156
+
157
+ ## Important notes:
158
+ 1. **Bilibili has no official personal Bot API** — this adapter uses the web DM interface via simulated login (unofficial)
159
+ 2. **Throttle your sends**: high-frequency / bulk DMs easily trigger risk control (DM功能 temporary ban); add rate limiting on the module side
160
+ 3. Do not use for harassment or bulk marketing; follow Bilibili community rules
161
+ 4. Cookies equal full account access — keep them safe; credential fields are masked in the panel
162
+ 5. Only direct chats are supported; fan-group chats are not
163
+ 6. Inbound pictures become image segments (URL); outbound pictures accept URL / local path
164
+ 7. When the login state expires (cookies stale) the adapter logs errors — re-scan or update cookies
165
+
166
+ ---
167
+
168
+ ### Reference links
169
+ - [ErisPulse main repository](https://github.com/ErisPulse/ErisPulse/)
170
+ - [bilibili-api-python](https://github.com/Passkou/bilibili-api)
171
+ - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
172
+ - [Module development guide](https://www.erisdev.com/#docs/developer-guide/README.md)
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ErisPulse-BilibiliAdapter"
7
+ version = "0.1.0"
8
+ description = "ErisPulse Bilibili 私信适配器 — Cookie/扫码双登录、事件驱动收发、多账户"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "ErisPulse>=2.7.1",
14
+ "bilibili-api-python>=16.3.0",
15
+ ]
16
+
17
+ [project.entry-points."erispulse.adapter"]
18
+ bilibili = "BilibiliAdapter:BilibiliAdapter"
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["BilibiliAdapter"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+