move live source ownership to accounts

This commit is contained in:
2026-07-18 22:24:00 -07:00
parent 1598d3c403
commit 53ae90ec13
20 changed files with 219 additions and 124 deletions
+7 -5
View File
@@ -5,8 +5,8 @@
## 核心目标
- 每个账户固定绑定一个 Bilibili 直播间和一个独立直播源。
- 一个直播源只建立一条上游连接,但可以把事件投递给多个组件实例。
- 每个账户固定绑定一个 Bilibili 直播间、一个 CookieCloud 来源和一条独立监听连接。
- 组件不绑定或选择直播源;账户事件流会提供给该账户所有启用的组件实例。
- 平台原始命令先转换成稳定的领域事件,组件不直接依赖 Bilibili `CMD`。
- HTTP 会话、直播源、组件、OBS token 和实时通道均以租户为边界。
- 新组件可以增加设置、投影和持久化副作用,而不修改直播连接核心。
@@ -32,7 +32,9 @@ flowchart LR
1. `SourceSupervisor` 为每个 `source_id` 保持至多一个 provider task。
2. `BilibiliProvider` 使用该用户加密保存的 CookieCloud 凭据构造 `libilibili`
客户端;crate 负责 WBI、WebSocket 与压缩包解析,adapter 再把强类型命令转换成 `LiveEvent`。
3. `SourceEventRouter` 同时使用 `owner_id` 与 `source_id` 查找启用的组件,并再次检查组件归属。
3. `SourceEventRouter` 按 `owner_id`
取得该账户所有启用组件,再由各组件的订阅声明筛选事件类型;`source_id`
只标识账户级监听,不存储在组件行中。
4. 匹配订阅后,路由器先执行可持久化的 `EventHandler`,再执行无副作用的 `EventProjection`。
5. 投影结果只发布到该 `component_id` 的广播通道。没有全局 WebSocket 事件总线。
6. OBS 使用组件级只读 token 订阅一个组件,不能读取控制台 API。
@@ -42,8 +44,8 @@ flowchart LR
| 状态 | 权威来源 | 内存副本 | 说明 |
| ---------------- | ------------ | ------------------------ | ----------------------------- |
| 用户、TOTP、会话 | PostgreSQL | 无 | Secret 加密,token 只保存摘要 |
| CookieCloud 凭据 | PostgreSQL | provider 构建期间解密 | 不返回浏览器 |
| 直播源和房间 | PostgreSQL | `SourceSupervisor` | 每用户固定一个房间 |
| CookieCloud 凭据 | PostgreSQL | provider 构建期间解密 | 每账户一份,不返回浏览器 |
| 直播源和房间 | PostgreSQL | `SourceSupervisor` | 每账户固定一个房间和连接 |
| 组件实例与设置 | PostgreSQL | `InMemoryComponentStore` | 写入成功后刷新热路径缓存 |
| 礼物/表情目录 | Bilibili API | provider catalog | 刷新失败保留最近成功快照 |
| 实时消息 | provider | `EventHub` 有界广播 | 不作为业务持久化机制 |
+5 -5
View File
@@ -1,6 +1,6 @@
# 组件开发指南
组件是“一个直播源上的独立功能实例”。当前内建 `danmaku_overlay` 与
组件是“消费所属账户事件流的独立功能实例”。当前内建 `danmaku_overlay` 与
`song_request`,未来礼物墙或统计组件也应使用同一套契约。
## 一个组件由什么组成
@@ -10,7 +10,7 @@
| 定义 | `ComponentDefinition` | kind、设置版本、默认值、校验、迁移、订阅 |
| 投影 | `EventProjection` | 把 `LiveEvent` 转成浏览器消息,不执行持久化副作用 |
| Handler | `EventHandler` | 可选的数据库写入、点歌或外部动作 |
| 实例 | `ComponentInstance` | owner、source、kind、名称、设置和启用状态 |
| 实例 | `ComponentInstance` | owner、kind、名称、设置和启用状态 |
| 实时通道 | `EventHub` | 按 component ID 隔离的有界广播 |
| 前端 | React renderer/editor | 管理设置、测试与 OBS 展示 |
@@ -25,7 +25,7 @@
4. 实现无副作用的 `EventProjection`。返回 `None` 表示该事件无需发送浏览器。
5. 若需要可靠业务动作,实现 `EventHandler`:
- 即使没有 OBS 客户端也会执行。
- 数据库操作必须包含 owner/source/component 条件。
- 数据库操作必须包含 owner/component 条件。
- 上游可能重试或出现组合事件,因此 handler 自己负责幂等。
6. 在 `ComponentRegistry::with_builtin_components` 注册定义与投影,再注册 handler。
7. 增加数据库创建/设置 API;不要把组件专属关系数据无限塞入 JSON settings。
@@ -49,8 +49,8 @@ Handler 面向“业务事实”。例如点歌请求、礼物累计或审计写
## 租户与 token 规则
- 组件必须属于同一个 owner 与 source。
- 路由和数据库查询都要重复检查这一关系。
- 组件必须属于经过认证的 owner,不能选择或覆盖账户直播源。
- 路由和数据库查询都要重复检查 owner;账户事件可被其所有启用组件订阅。
- 每个组件单独签发 access token,默认只有 `events:subscribe`。
- 删除组件或轮换 token 时,关闭该组件 channel,使已有 socket 立即失效。
- 组件 WebSocket 不得暴露其他组件列表或控制 API。
+3 -1
View File
@@ -65,7 +65,9 @@ close 结束连接。
```
`ownerId` 永远不会序列化到浏览器。消费者应按 `version` 和 `type`
分派,并忽略不认识的 payload 字段,从而允许兼容地增加元数据。
分派,并忽略不认识的 payload 字段,从而允许兼容地增加元数据。 `sourceId`
标识所属账户唯一的直播监听连接,不表示组件单独绑定了一个直播源;同一账户不同组件收到的实时事件具有相同的
`sourceId`。
## 事件类型
+3 -2
View File
@@ -6,12 +6,13 @@ token。以下规则是实现约束,而不是可选部署建议。
## 租户隔离
- HTTP handler 只从服务端会话解析 `owner_id`,不接受客户端声明的 owner。
- 组件查询同时限定 `owner_user_id` 与 `source_id`。
- 组件查询始终限定 `owner_user_id`;组件表不保存直播源外键。
- 直播间、直播源和 CookieCloud 凭据分别以账户 ID 建立唯一约束与 RLS 边界。
- 数据库使用 owner 复合外键、RLS 和 `FORCE ROW LEVEL SECURITY`。
- tenant 查询必须在事务中执行 `SET LOCAL app.user_id`,不能使用会泄漏到连接池的 session-level
`SET`。
- 实时广播按 `component_id` 建立独立 channel,不提供全局订阅。
- 路由器在投影发布前再次验证 owner、source 和 component ID。
- 路由器按事件的可信 `owner_id` 扇出,并在投影发布前再次验证 owner、账户 source 和 component ID。
## Secret 生命周期