# 组件实时协议 当前协议版本为 `1`。后端的权威定义在 `domain.rs` 的 `ComponentMessage`,浏览器渲染器只依赖本文列出的稳定字段。 ## 连接地址与认证 组件流地址: ```text wss://danmaku.luoxingci.com/api/v1/components//stream ``` OBS 页面地址中的 token 位于 fragment: ```text https://danmaku.luoxingci.com/obs/#token= ``` fragment 不会进入首次 HTTP 请求或 Nginx access log。WebSocket 建立后,客户端必须在 8 秒内发送第一帧: ```json { "type": "authenticate", "token": "component-token" } ``` 服务端验证 token 摘要、组件归属和 `events:subscribe` scope。认证成功后返回: ```json { "version": 1, "type": "authenticated", "componentId": "08d31d19-3e4b-4c11-9cf7-9bd786a39465", "componentKind": "song_request" } ``` 之后立即发送通用的 `component.settings.snapshot`,再发送该 kind 的状态快照和实时事件。弹幕姬还会发送 `overlay.settings.snapshot` 兼容帧。token 无效、被轮换或属于其他组件时,服务端以 policy close 结束连接。 设置快照中的 `themeId` 是稳定的主题标识,目前支持 `jade-scroll`。消费者应把未知主题降级为自身默认主题,不能因主题发布顺序不同而中断实时消息。 ## 版本化事件信封 ```json { "version": 1, "id": "d56c1a28-a6f1-4ad9-b02f-4c22dc4d3c27", "componentId": "08d31d19-3e4b-4c11-9cf7-9bd786a39465", "sourceId": "c8087bb0-0dde-40eb-a43d-a9a2574c2717", "occurredAt": "2026-07-15T20:10:30.125Z", "roomId": "123456", "type": "live.danmaku", "payload": { "viewer": { "uid": "42", "name": "观众" }, "text": "晚上好", "segments": [{ "type": "text", "text": "晚上好" }] } } ``` `ownerId` 永远不会序列化到浏览器。消费者应按 `version` 和 `type` 分派,并忽略不认识的 payload 字段,从而允许兼容地增加元数据。 ## 事件类型 | `type` | 主要 payload | 说明 | | ----------------------------- | --------------------------------------------- | ---------------------- | | `component.settings.snapshot` | `settings` | 所有组件的设置快照 | | `component.settings.updated` | `settings` | 所有组件的设置更新 | | `overlay.settings.snapshot` | `settings` | 认证后当前设置快照 | | `overlay.settings.updated` | `settings` | 控制台保存后的实时设置 | | `live.danmaku` | `viewer`, `text`, `segments` | 普通文字和表情分段 | | `live.enter` | `viewer` | 进房事件 | | `live.gift` | `viewer`, `gift`, `quantity`, `sourceEventId` | 一次可记账的礼物事件 | | `live.gift.combo` | `viewer`, `gift`, `quantity`, `comboId` | 同一连击的视觉更新 | | `live.superchat` | `viewer`, `message`, `price`, `sourceEventId` | 醒目留言 | | `live.guard.buy` | `viewer`, `guardName`, `quantity`, `price` | 舰长购买 | | `live.like` | `viewer` | 点赞 | | `live.share` | `viewer` | 分享 | | `live.unknown` | `command`, `metadata` | 有界且已清理的未知事件 | ## 点歌姬状态 `song_request` 认证后按顺序接收: 1. `song.queue.snapshot.begin`:包含 `snapshotId`、`revision`、当前歌曲和待唱总数。 2. 零到多个 `song.queue.snapshot.page`:每页最多 100 个待唱项。 3. `song.queue.snapshot.end`:提交该快照。 后续 `song.queue.changed` 携带连续 revision,`operation` 为 `added`、`promoted`、 `completed`、`cancelled` 或 `rating-updated`。客户端发现 revision 缺口必须重连取得新快照,不能猜测缺失队列状态。 礼物目录价格的原始单位是人民币的千分之一。`gift.totalPrice` 保留该整数单位,`gift.priceCny` 是供展示使用的人民币数值。 `live.gift` 与 `live.gift.combo` 不是两笔礼物。需要持久化计数的组件通常只消费 `live.gift`;连击事件用于更新同一张视觉卡片。 ## 表情分段 `live.danmaku.payload.segments` 是判别联合: - `text`:包含可直接显示的文字。 - `emoticon`:包含回退文字、图片 URL、可选尺寸、动态标记和整条大表情标记。 图片失败时客户端必须回退到 `text`,不能让一张失效图片破坏整条弹幕。 ## 背压与重连 - 每个组件使用有界 `tokio::broadcast` channel。 - 慢客户端发生 `Lagged` 时跳过已经丢失的旧帧,继续接收新事件;实时展示不保证历史重放。 - 浏览器对非鉴权关闭使用指数退避重连,上限 12 秒。 - 鉴权失败不会自动重试,避免对已撤销 token 形成无限请求。 - 需要可靠业务处理的功能必须实现 `EventHandler` 并写入数据库,不能把 WebSocket 当作消息队列。