首次推送

This commit is contained in:
sansen
2026-07-20 19:01:03 +08:00
parent ea01b9cf99
commit ef1bf61f9f
4484 changed files with 937163 additions and 1 deletions
+452
View File
@@ -0,0 +1,452 @@
# 流程文档总览
> 本文档按用户流程组织,将 SDK 技术行为拆分为七个主流程文件和一个独立的状态同步流程文件。
> 依据:`docs/sdk-bridge-api.md`
> SDK`github.com/honeybbq/teamspeak-go` → gomobile AAR → `TSBridge.kt` 应用层桥接
---
## 一、完整用户流程
```
进入应用
├─ 已有最近服务器? ──是──→ 显示最近服务器列表 ──→ 选择服务器
│ │
└─ 否 ──→ 输入服务器地址 ──→ 输入昵称 ──→ 输入密码(可选) ──→┘
点击"进入服务器"
┌─────────────────────┘
连接中(显示进度)
┌───────────────┼───────────────┐
▼ ▼ ▼
连接成功 连接失败 超时
│ │ │
│ 显示错误原因 显示超时提示
│ 允许重试 允许重试
│ │ │
│ └───────→ 返回输入页面
分配到默认频道(首次同步 ①)
浏览频道列表 ──→ 浏览频道中的人员分布
│ │
│ poke 服务器内成员
│ │
▼ ▼
┌──→ 进入/切换频道 ←──────────┘
│ │ ← 自身状态同步 ④
│ │ ← 增量同步 ② / 补偿同步 ③
│ │
│ ├─ 频道有密码? ──是──→ 输入密码
│ │
│ ├─ 收听语音 ──→ 发送语音(PTT)
│ │
│ ├─ 收到文本消息 ──→ 消息归档同步 ⑤ ──→ 发送文本消息
│ │
│ ├─ 浏览频道文件 ──→ 上传文件 ──→ 下载文件
│ │
│ ├─ poke 服务器内成员 ──→ 被 poke
│ │
│ ▼
│ 切换频道 / 留在当前频道
│ │
└────┘
┌─── 离开原因 ───┐
│ │ │
▼ ▼ ▼
主动断开 网络异常 被踢出
│ │ │
▼ ▼ ▼
发送 显示错误 显示被踢原因
disconnect 允许重连 允许重连
│ │ │
│ 重连后全量同步 ⑥
│ │ │
└────────┼─────────┘
清理会话状态
返回应用主页
```
> **① 首次同步** ② **增量同步** ③ **补偿同步** ④ **自身状态同步** ⑤ **消息归档同步** ⑥ **重连后全量同步**
> 详见 [08_状态同步.md](08_状态同步.md)
---
## 二、用户流程对应的程序流程概览
### 2.1 连接服务器
```
读取本地配置
检查输入是否合法
创建 Identity(首次生成,后续读取)
TSBridge.connect(host, nickname, password, defaultChannel, defaultChannelPassword, callbacks)
callbacks 内部注册 EventBus.emit()(将 JNI 回调转为 EventBus 事件)
TSClient.connect() → 建立 UDP 连接 → 身份认证 → 服务器密码验证
├── 失败 → 连接失败处理 → 显示错误
└── 成功 →
EventBus 收到 Connected 事件
开始同步服务器数据
├── 获取频道列表 TSBridge.getChannelsJSON()
├── 获取成员列表 TSBridge.getClientsJSON()
└── 获取自身 ID TSBridge.getClientID()
建立本地状态(频道基线 + 成员基线)
UI 切换到服务器页面
```
### 2.2 浏览频道与成员
```
已同步的频道列表 → 构建树形结构 → 渲染频道树
已同步的成员列表 → 按 channelID 索引 → 派生每个频道的成员列表
持续接收 EventBus 事件(debounce 合并后处理):
├── ClientEnter → 新增成员到成员表
├── ClientMoved → 更新成员所在频道
├── ClientLeave → 从成员表移除
└── Poked → 显示 Poke 通知
未知 clientID → 触发 TSBridge.getClientsJSON() 补偿同步
未知 channelID → 触发 TSBridge.getChannelsJSON() 补偿同步
```
### 2.3 切换频道
```
用户选择目标频道
├── 频道有密码? → 弹出密码输入
├── TsClient.clientMove(selfID, targetID, password)
├── 命令被拒绝 → 显示失败原因 → 允许重试
└── 命令成功 → 等待 onClientMoved 服务端事实
比对 clientID == selfID
提交新频道事实 → UI 导航到目标频道
```
### 2.4 发送与接收文本消息
```
发送:
用户输入消息 → 选择目标模式(频道/私聊/服务器)
TSBridge.sendTextMessage(targetMode, targetId, message)
├── 失败 → 标记消息发送失败 → 允许重试
└── 成功 → 标记发送完成
接收:
服务器推送 notifytextmessage
EventBus 收到 TextMessage 事件(立即分发,不合并)
按 targetMode 与 target 归档(不用当前页面猜测)
更新目标会话和未读状态
```
### 2.5 语音通信
```
用户按下 PTT 按钮
├── 前置条件检查(已连接 + 已在频道中)
│ └── 不满足 → 显示无法发言原因
└── 满足 → 启动麦克风采集 + Opus 编码(Dispatchers.IO
循环发送:
└── 每帧 → TSBridge.sendVoice(data, codec)
├── 用户松开 → StopCapture → 恢复静默
└── 连接断开 → StopCapture → 进入 Blocked
接收语音(绕过 EventBus,延迟敏感):
VoiceService.handleVoiceData() → Opus 解码 → AudioTrack 播放
```
### 2.6 文件传输
```
上传:
选择文件 → FileTransferInitUpload(channelID, path, size, overwrite)
获得 host/port/key
DialFileTransfer(host, port, key) → 建立 TCP 连接
UploadFileData(host, info, reader) → 传输文件字节流
下载:
选择文件 → FileTransferInitDownload(channelID, path, password)
获得 host/port/key
DialFileTransfer(host, port, key) → 建立 TCP 连接
DownloadFileData(host, info, writer) → 接收文件字节流
```
### 2.7 断开连接
```
主动断开:
用户点击离开 → 状态改为 disconnecting → 阻止新操作
TSBridge.disconnect() → 通知服务器客户端主动离开
EventBus 收到 Disconnected 事件 → 停止语音和文件传输 → 清理会话状态 → 返回主页
被动断开:
UDP 会话中断 → EventBus 收到 Disconnected(error) → 显示错误原因 → 允许重连
被踢出:
notifyclientleftview reasonid=4/5 → EventBus 收到 Kicked(reason) → 显示被踢原因 → 允许重连
```
### 2.8 状态同步(独立于主流程的横切关注点)
```
贯穿所有主流程的同步机制:
① 首次同步(连接建立后)
触发:EventBus 收到 Connected 事件
动作:TSBridge.getChannelsJSON() + TSBridge.getClientsJSON() + TSBridge.getClientID()
目标:建立完整的频道基线和成员基线
② 增量同步(事件驱动)
触发:EventBus 收到 ClientEnter / ClientMoved / ClientLeave 事件
动作:debounce 合并后 refreshClientList() 全量刷新
目标:维护实时成员位置和频道人数
③ 补偿同步(数据不一致时)
触发:事件引用了未知的 clientID 或 channelID
动作:重新调用 TSBridge.getClientsJSON() 或 TSBridge.getChannelsJSON()
目标:用完整列表修复丢失的基线数据
④ 自身状态同步(频道切换后)
触发:EventBus 收到自己的 ClientMoved 事件
动作:比对 clientID == selfID → 更新自身所在频道
目标:确认当前用户的实际频道位置
⑤ 消息归档同步(收到消息时)
触发:EventBus 收到 TextMessage 事件
动作:按 targetMode + target 归档到正确的会话
目标:维护消息与目标频道的对应关系
⑥ 重连后全量同步(断开重连后)
触发:重新收到 Connected 事件
动作:清空旧会话状态 → 重新执行首次同步
目标:确保重连后数据与服务器完全一致
⑦ 同步失败处理
触发:TSBridge.getChannelsJSON() / TSBridge.getClientsJSON() 请求失败
动作:标记同步失败 → 不进入业务就绪 → 允许重试
目标:防止在不完整数据上执行业务操作
```
---
## 三、文件索引
### 主流程
| 文件 | 用户流程 | 主要内容 |
| --- | --- | --- |
| [01_连接服务器.md](01_连接服务器.md) | 输入信息 → 建立连接 → 首次同步 | 生命周期、初始化配置、构造连接时序、状态树、事件依赖 |
| [02_浏览频道.md](02_浏览频道.md) | 浏览频道列表 → 查看成员 → Poke | 同步状态、成员状态、成员事件时序、成员实体状态树 |
| [03_切换频道.md](03_切换频道.md) | 选择频道 → 输入密码 → 等待确认 | 频道切换时序、切换状态机 |
| [04_文本消息.md](04_文本消息.md) | 发送消息 → 接收消息 → 消息归档 | 文本消息时序、聊天状态树 |
| [05_语音通信.md](05_语音通信.md) | PTT 按下 → 发送语音帧 → 松开 | 语音发送时序、语音状态树 |
| [06_文件传输.md](06_文件传输.md) | 初始化 → TCP 连接 → 数据传输 | 文件传输时序、文件状态树 |
| [07_断开连接.md](07_断开连接.md) | 主动断开 / 被动断开 / 被踢 | 断开时序、清理流程 |
| [09_EventBus架构.md](09_EventBus架构.md) | TS 事件与渲染线程分离 | 事件定义、合并策略、线程模型 |
> **注**:流程文档中的 API 调用通过 `TSBridge` 单例,事件通过 `EventBus` 接收。
> 底层通过 gomobile AAR 调用 Go SDK。详见 [sdk-bridge-api.md](../sdk-bridge-api.md) 和 [09_EventBus架构.md](09_EventBus架构.md)。
### 横切流程
| 文件 | 说明 | 触发时机 |
| --- | --- | --- |
| [08_状态同步.md](08_状态同步.md) | 独立于主流程之外的频道/成员/消息状态同步 | 连接建立、事件到达、数据不一致、重新连接时触发 |
> 所有流程文档中的 API 调用通过 `TSBridge` 单例,事件通过 `EventBus` 接收。
> 底层通过 gomobile AAR 调用 Go SDK。
---
## 四、图例
本文将 SDK 行为分成三类:
| 类型 | 含义 | Kotlin API 示例 |
| --- | --- | --- |
| 本地调用 | 只在客户端进程内执行,不直接产生网络通信 | `TsClient.getClientId()``TsClient.getSelf()` |
| 客户端请求 | 客户端主动向 TeamSpeak 服务器发送命令并等待响应 | `TsClient.connect()``TsClient.getChannelList()``TsClient.clientMove()` |
| 服务端推送 | TeamSpeak 服务器主动下发通知,由 SDK 转换成 EventBus 事件 | `ClientEnter``ClientMoved``TextMessage` |
所有 Mermaid 图块均使用"技术名称 + 中文注释"的形式。箭头表示调用、数据流或前置依赖,不表示所有步骤都能互换顺序。
---
## 五、运行架构
### SDK 内部与上层应用的运行关系
```mermaid
flowchart TB
subgraph APP[上层应用层<br/>负责 UI、业务状态和用户意图]
UI[界面与交互<br/>发起连接、切换、聊天和 PTT]
SM[业务状态机<br/>管理连接、同步、成员和语音状态]
STORE[实体与消息存储<br/>保存频道、客户端和聊天事实]
end
subgraph BRIDGE[桥接层<br/>TSBridge 单例<br/>负责 Kotlin 友好封装与生命周期管理]
TSB[TSBridge<br/>connect、getChannelsJSON、moveToChannel 等 API]
CB[JNI EventCallback<br/>Go 回调转 EventBus 事件]
end
subgraph EB[事件总线层<br/>EventBus<br/>负责事件收集、合并与分发]
BUS[EventBus<br/>SharedFlow 事件流]
MERGE[事件合并器<br/>debounce 高频成员变化]
THREAD[线程调度<br/>JNI 线程 → Main 线程]
end
subgraph SDK[teamspeak-go SDK (gomobile AAR)<br/>负责协议、连接和事件转换]
API[Go 公开方法<br/>Connect、GetChannelsJSON、SendVoice 等]
CMDMW[命令中间件链<br/>拦截发送前的客户端请求]
CMDBUS[命令通道<br/>构建、转义并发送协议命令]
EVTBUS[事件分发器<br/>接收并解析 notify* 通知]
EVTMW[事件中间件链<br/>拦截分发前的服务端事件]
HANDLER[Go 回调 → gomobile 转换<br/>将 Go 结构体转为 Java 对象]
VOICE[语音通道<br/>发送原始 Opus 语音帧]
FILE[文件传输协调器<br/>初始化上传或下载会话]
end
subgraph NET[网络与服务器层<br/>负责 TeamSpeak 实际通信]
UDP[UDP 协议连接<br/>承载握手、命令、事件和语音]
TCP[文件传输 TCP 连接<br/>承载上传和下载数据]
SERVER[TeamSpeak 服务器<br/>提供频道、成员、消息和权限]
end
UI --> SM
SM --> TSB
TSB --> API
API --> CMDMW
CMDMW --> CMDBUS
CMDBUS --> UDP
UDP <--> SERVER
SERVER --> UDP
UDP --> EVTBUS
EVTBUS --> EVTMW
EVTMW --> HANDLER
HANDLER --> CB
CB --> BUS
BUS --> MERGE
MERGE --> THREAD
THREAD --> SM
SM --> STORE
API --> VOICE
VOICE --> UDP
API --> FILE
FILE --> TCP
TCP <--> SERVER
```
### 三条核心运行通道
**命令通道**
```text
TSBridge → gomobile → Go SDK API → CommandMiddleware → 协议命令 → UDP → 服务器响应
```
适用:`TSBridge.getChannelsJSON()``TSBridge.getClientsJSON()``TSBridge.moveToChannel()``TSBridge.sendChannelMessage()``TSBridge.sendVoice()`
**事件通道**
```text
服务器 notify* → UDP → Go SDK 解析 → EventMiddleware → gomobile 回调 → TSBridge EventCallback → EventBus → ViewModel → StateFlow → UI
```
适用:`Connected``Disconnected``ClientEnter``ClientLeave``ClientMoved``TextMessage``Poked``Kicked`
**语音通道(绕过 EventBus**
```text
Go 语音包 → JNI OnVoiceData → VoiceService (Dispatchers.IO) → Opus 解码 → AudioTrack 播放
```
语音数据延迟敏感,不经过 EventBus,由 VoiceService 在 IO 线程直接处理。
**文件传输通道**
```text
初始化命令 → 获得 host/port/key → DialFileTransfer → TCP 数据传输
```
文件内容不直接复用普通命令通道,初始化结果是建立 TCP 传输连接的前置条件。
### 中间件执行方向
```mermaid
flowchart LR
A[上层业务命令<br/>通过 TSBridge 调用 API] --> B[CommandMiddleware 外层<br/>执行日志、鉴权或限流]
B --> C[CommandMiddleware 内层<br/>修改或检查协议命令]
C --> D[SDK 命令发送器<br/>编码并发送 TeamSpeak 命令]
D --> E[TeamSpeak 服务器<br/>执行命令并返回响应]
E --> F[SDK 事件解析器<br/>解析 notify* 服务端通知]
F --> G[EventMiddleware 内层<br/>检查或转换原始事件]
G --> H[EventMiddleware 外层<br/>记录事件或统一过滤]
H --> I[gomobile 回调 → TSBridge<br/>JNI 回调转 EventBus 事件]
I --> J[EventBus<br/>合并/节流后分发给 ViewModel]
```
---
## 六、统一实现原则
1. **先注册事件,再连接。** 避免握手完成后的首批推送无人处理。
2. **先等待连接就绪,再发送业务命令。** `Connect` 不代替 `WaitConnected`
3. **列表命令建立基线,服务端事件维护增量。** 两者缺一不可。
4. **成员表按 ClientID 幂等更新。** 不使用独立 `+1/-1` 长期维护频道人数。
5. **命令响应和事件事实分离。** 尤其是频道移动,必须等待 `OnClientMoved`
6. **当前用户通过 ClientID 识别。** 不依赖昵称或 UI 当前页面判断自己。
7. **未知实体引用触发补偿同步。** 不静默忽略移动或离开事件。
8. **消息按 TargetMode 和 Target 归档。** 不使用当前页面频道代替真实目标。
9. **语音只在连接和频道前置条件满足时发送。** 断开或被踢必须立即停止。
10. **文件传输严格遵守"初始化 → TCP 连接 → 数据传输"。** 三个阶段不能跳过。
11. **中间件必须正确调用下一层。** 吞掉命令会导致请求不发送,吞掉事件会导致状态缺失。
12. **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
13. **事件通过 EventBus 分发,不在 JNI 回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`ViewModel 通过 `collect()` 响应。高频成员变化事件 debounce 合并,避免事件风暴。
+244
View File
@@ -0,0 +1,244 @@
# 连接服务器
> 用户流程:输入服务器地址 → 输入昵称 → 输入密码(可选) → 点击进入服务器 → 连接成功 → 首次同步
> 对应程序流程:准备配置 → TSBridge.connect() → EventBus 收到 Connected 事件 → 首次同步
---
## 一、生命周期
### 1.1 完整生命周期
```mermaid
flowchart TD
A[准备配置<br/>地址、昵称、密码] --> B[TSBridge.connect<br/>组装连接参数并注册 EventCallback]
B --> C[Go SDK Connect<br/>通过 gomobile 发起 UDP 会话与协议握手]
C --> D{连接请求是否成功<br/>判断 connect 返回值}
D -- 否 --> E[连接失败处理<br/>记录错误并释放客户端资源]
D -- 是 --> F{是否已收到 Connected 事件<br/>等待 EventBus 连接就绪事件}
F -- 否 --> G[等待失败处理<br/>处理超时或握手异常]
F -- 是 --> H[EventBus Connected 事件<br/>接收连接成功事件]
H --> I[首次同步<br/>请求频道、成员和自身身份信息]
I --> J[运行就绪<br/>允许发送命令、聊天和语音]
J --> K[持续接收 EventBus 事件<br/>归并成员、移动、消息和 Poke]
K --> J
J --> L{结束原因<br/>区分主动断开与被动中断}
L -- 用户主动退出 --> M[TSBridge.disconnect<br/>发送优雅关闭请求]
L -- 网络或服务端中断 --> N[EventBus Disconnected 事件<br/>接收断开原因并停止写操作]
L -- 自己被踢出 --> O[EventBus Kicked 事件<br/>接收频道踢或服务器踢原因]
M --> P[清理运行资源<br/>停止语音、取消请求并清空会话状态]
N --> P
O --> P
P --> Q[生命周期结束<br/>不再承担当前会话请求]
```
### 生命周期约束
1. `TSBridge.connect()` 发起连接,连接就绪通过 EventBus `Connected` 事件确认。
2. EventBus 事件监听应在 `connect()` 之前开始收集,避免连接早期事件无人接收。
3. `connect()` 成功只表示连接流程已启动;发送普通命令前必须等待 `Connected` 事件。
4. `Connected` 是 SDK 推送的已连接事实,可作为首次同步入口。
5. `TSBridge.disconnect()` 用于主动优雅退出;`Disconnected` 事件用于观察最终断开结果或异常断开。
6. `Kicked` 事件来源于 `notifyclientleftview` 的特定原因,必须与普通成员离开区分。
7. 断开后应停止语音、拒绝新命令,并清理只属于当前会话的状态。
### 1.2 生命周期阶段与允许操作
| 阶段 | 允许操作 | 禁止或不建议操作 | 中文说明 |
| --- | --- | --- | --- |
| 构造前 | 准备配置 | 调用任何 TSBridge 方法 | 连接尚未建立 |
| 已配置 | 注册 EventBus 事件收集、读取本地配置 | 发送服务器命令 | 尚未建立网络会话 |
| 连接中 | 等待 `Connected` 事件、响应取消 | `getClientsJSON()``moveToChannel()` 等业务命令 | 握手尚未确认完成 |
| 已连接 | 同步频道和成员、发送聊天、发送语音 | 重复调用 `connect()` | SDK 已具备业务通信条件 |
| 断开中 | 停止上层写操作、等待清理 | 发起新业务命令 | 会话正在关闭 |
| 已断开 | 释放资源或重新连接 | 复用失效会话发送命令 | 旧会话不再可信 |
---
## 二、初始化配置
### 2.1 初始化配置结构
```mermaid
flowchart TD
A[应用启动配置<br/>收集身份、地址、昵称和可选参数] --> B[Identity<br/>提供 TeamSpeak 加密身份]
A --> C[服务器地址 addr<br/>提供域名、IP 或 TSDNS 地址]
A --> D[昵称 nickname<br/>提供当前连接显示名称]
A --> E[连接选项<br/>提供可选运行能力]
E --> E3[serverPassword<br/>设置服务器连接密码]
E --> E4[defaultChannel<br/>设置连接后默认进入的频道名称]
E --> E5[defaultChannelPassword<br/>设置默认频道密码]
B --> F[TSBridge.connect<br/>组装参数并通过 gomobile 发起连接]
C --> F
D --> F
E3 --> F
E4 --> F
E5 --> F
F --> G[注册 EventCallback<br/>JNI 回调转 EventBus 事件]
G --> H[Go SDK Connect<br/>UDP 会话与协议握手]
```
### 2.2 推荐初始化顺序
```kotlin
// TSBridge.connect() 内部注册 JNI EventCallback
// 将所有 Go 回调转为 EventBus.emit() 事件。
// ViewModel 通过 EventBus.events.collect() 接收事件并更新状态。
// 示例:ServerViewModel 中收集连接事件
viewModelScope.launch {
EventBus.events.collect { event ->
when (event) {
is TSEvent.Connected -> {
// 开始首次同步
performInitialSync()
}
is TSEvent.Disconnected -> {
// 清理会话
handleDisconnect(event.message)
}
else -> { /* 其他事件由对应 ViewModel 处理 */ }
}
}
}
// connect 发起网络连接,Connected 事件确认握手真正完成。
val error = TSBridge.connect(host, nickname, password, defaultChannel, defaultChannelPassword, callbacks)
// error 为空字符串表示连接流程已启动,等待 EventBus Connected 事件
```
### 2.3 配置之间的依赖
| 配置 | 前置条件 | 影响阶段 | 依赖说明 |
| --- | --- | --- | --- |
| `Identity` | 必须可用 | 连接、握手 | 缺少身份无法正确创建客户端 |
| `addr` | 必须非空且可解析 | 地址解析、连接 | 由 SDK 内部解析器解析 |
| `nickname` | 必须满足服务器命名规则 | 握手、上线 | 服务器可能拒绝无效或冲突昵称 |
| `serverPassword` | 服务器启用密码时需要 | 握手 | 密码错误会导致连接失败 |
| `defaultChannel` | 目标频道名称存在 | 连接完成阶段 | SDK 尝试在连接后自动进入该频道 |
| `defaultChannelPassword` | 已设置默认频道且频道有密码 | 进入默认频道 | 单独设置密码而无默认频道没有明确目标 |
---
## 三、状态树
### Client 总状态树(连接相关部分)
```mermaid
flowchart TD
ROOT[Client 生命周期状态树<br/>描述一个连接从创建到结束的完整状态]
ROOT --> U[未配置 Uninitialized<br/>仅准备 Identity 和连接参数]
ROOT --> C[已配置 Configured<br/>EventBus 已注册但尚未连接]
ROOT --> N[连接过程 Connecting<br/>正在解析地址并执行握手]
ROOT --> R[已连接 Connected<br/>握手完成且可以执行业务命令]
U --> U1[配置 Identity<br/>准备加密身份]
U --> U2[配置地址与昵称<br/>准备基础连接参数]
U --> U3[配置连接选项<br/>准备密码和默认频道]
C --> C1[注册 EventBus 事件收集<br/>绑定服务端推送处理器]
C --> C2[等待 connect 调用<br/>尚不能发送业务命令]
N --> N1[Resolving<br/>SDK 内部解析地址]
N --> N2[Handshaking<br/>通过 UDP 建立协议会话]
N --> N3[WaitingReady<br/>等待 Connected 事件]
N --> N4[ConnectFailed<br/>解析、密码、网络或握手失败]
R --> R1[Syncing<br/>通过列表命令建立频道和成员基线]
R --> R2[Ready<br/>允许聊天、移动、查询和语音]
```
---
## 四、时序:构造、连接与首次同步
```mermaid
sequenceDiagram
participant UI as 上层应用<br/>发起连接并展示状态
participant VM as ViewModel<br/>收集 EventBus 事件
participant BRIDGE as TSBridge<br/>管理连接生命周期
participant SDK as Go SDK (gomobile)<br/>管理协议通信
participant TS as TeamSpeak 服务器<br/>执行握手和列表请求
participant STORE as 业务状态仓库<br/>保存频道和成员基线
UI->>BRIDGE: connect(host, nickname, password, ...)
BRIDGE->>SDK: gomobile 调用 Go Connect
SDK->>TS: 连接握手请求<br/>协商身份、密码和会话
TS-->>SDK: 握手响应
SDK-->>BRIDGE: connect 返回 ""
SDK->>BRIDGE: OnConnected 回调<br/>gomobile 转换为 JNI 回调
BRIDGE->>BRIDGE: EventBus.emit(Connected)
BRIDGE->>VM: EventBus SharedFlow<br/>分发 Connected 事件
VM->>VM: 更新连接状态
par 请求频道基线
VM->>BRIDGE: TSBridge.getChannelsJSON()
BRIDGE->>SDK: gomobile 调用 GetChannelsJSON
SDK->>TS: channellist
TS-->>SDK: JSON 字符串
SDK-->>BRIDGE: 频道 JSON
BRIDGE-->>VM: JSON 字符串
VM->>STORE: 解析并保存频道基线
and 请求成员基线
VM->>BRIDGE: TSBridge.getClientsJSON()
BRIDGE->>SDK: gomobile 调用 GetClientsJSON
SDK->>TS: clientlist
TS-->>SDK: JSON 字符串
SDK-->>BRIDGE: 成员 JSON
BRIDGE-->>VM: JSON 字符串
VM->>STORE: 解析并保存成员基线
and 读取自身 ID
VM->>BRIDGE: TSBridge.getClientID()
BRIDGE-->>VM: selfClientID
VM->>STORE: 保存自身 ID
end
VM->>STORE: 原子提交频道、成员和自身 ID
STORE-->>UI: 同步完成<br/>连接状态进入业务就绪
```
---
## 五、事件依赖
### 连接相关前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `TSBridge.connect` | 地址、昵称 | 已注册 EventBus 事件收集 | 参数无效时不进入连接流程 |
| `connect` 成功 | 已注册 EventBus 事件收集 | — | 返回错误信息并停止等待连接 |
| `Connected` 事件 | 已调用 `connect` | — | 超时后终止本次初始化 |
| 首次同步 | `Connected` 事件 | 同时读取频道、成员和自身 ID | 任一核心请求失败则不标记业务就绪 |
### 事件依赖总图(连接阶段)
```mermaid
flowchart TD
CONFIG[初始化配置完成<br/>地址、昵称已准备] --> REG[注册 EventBus 事件收集<br/>确保早期服务端推送可被处理]
REG --> CONNECT[TSBridge.connect<br/>启动地址解析、UDP 会话和握手]
CONNECT --> CONNECTED[EventBus Connected 事件<br/>SDK 推送连接成功事实]
CONNECTED --> CID[TSBridge.getClientID<br/>读取当前用户客户端 ID]
CONNECTED --> CHANNELS[TSBridge.getChannelsJSON<br/>建立频道基线]
CONNECTED --> CLIENTS[TSBridge.getClientsJSON<br/>建立在线成员基线]
CID --> SELFREADY[当前用户身份就绪<br/>能够识别自己的移动和离开事件]
CHANNELS --> DATAREADY[服务器实体就绪<br/>频道和成员引用关系可被验证]
CLIENTS --> DATAREADY
SELFREADY --> READY[业务就绪<br/>允许移动、聊天和语音]
DATAREADY --> READY
```
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 是否完成连接 | EventBus `Connected` 事件 | `connect()` 只负责启动连接 |
| 自身客户端 ID | `TSBridge.getClientID()` | SDK 本地缓存服务器分配的 ID |
| 初始频道列表 | `TSBridge.getChannelsJSON()` 响应 | SDK 未列出频道创建、更新、删除事件 |
| 初始在线成员 | `TSBridge.getClientsJSON()` 响应 | 建立完整在线成员基线 |
+131
View File
@@ -0,0 +1,131 @@
# 浏览频道与成员
> 用户流程:分配到默认频道 → 浏览频道列表 → 浏览频道中的人员分布 → poke 服务器内成员
> 对应程序流程:TsClient.getChannelList() 建立频道基线 → TsClient.getClientList() 建立成员基线 → 持续接收 onClient* 事件增量更新
---
## 一、状态树
### 同步状态
```mermaid
flowchart TD
SYNC[同步状态<br/>建立服务器数据基线]
SYNC --> S0[未同步 Unsynced<br/>已连接但尚无完整数据]
SYNC --> S1[同步中 Syncing<br/>调用 getChannelList 和 getClientList]
SYNC --> S2[已同步 Synchronized<br/>列表基线可供 UI 使用]
SYNC --> S3[同步失败 SyncFailed<br/>列表请求失败或上下文失效]
```
### 成员状态
```mermaid
flowchart TD
MEMBER[成员状态<br/>维护在线用户及频道位置]
MEMBER --> M0[空基线 Empty<br/>尚未执行 ListClients]
MEMBER --> M1[已有基线 Loaded<br/>getClientList 已建立成员表]
MEMBER --> M2[增量更新 Updating<br/>OnClientEnter/Leave/Moved 正在归并]
MEMBER --> M3[不一致 Inconsistent<br/>事件引用未知成员或频道]
```
### 成员实体状态树
```mermaid
flowchart TD
ENTITY[成员实体状态树<br/>描述 ListClients 基线与实时事件如何协同]
ENTITY --> BASE[基线分支<br/>由客户端请求建立完整成员集合]
ENTITY --> DELTA[增量分支<br/>由服务端推送维护实时变化]
ENTITY --> SELF[当前用户分支<br/>通过 ClientID 识别自己的事件]
ENTITY --> REPAIR[修复分支<br/>处理事件缺失或引用不一致]
BASE --> B1[getClientList 请求<br/>获取服务器当前全部在线客户端]
B1 --> B2[按 clientID 建表<br/>使用客户端 ID 去重保存]
B2 --> B3[按 channelID 建索引<br/>派生每个频道的成员列表]
DELTA --> D1[onClientEnter<br/>新增或覆盖进入视野的成员]
DELTA --> D2[onClientMoved<br/>覆盖成员的 channelID]
DELTA --> D3[onClientLeave<br/>按 clientID 幂等删除成员]
SELF --> S1[getClientId 本地调用<br/>读取服务器分配的自身 ID]
S1 --> S2[事件 clientID 比对<br/>判断移动或离开事件是否属于自己]
S2 --> S3[更新自身频道事实<br/>自己的移动决定实际所在频道]
REPAIR --> R1[检测未知 clientID<br/>移动或离开事件找不到实体]
R1 --> R2[重新调用 getClientList<br/>用完整列表修复成员基线]
REPAIR --> R3[检测未知 channelID<br/>成员引用本地不存在的频道]
R3 --> R4[重新调用 getChannelList<br/>用完整列表修复频道基线]
```
---
## 二、时序:成员进入、移动与离开
```mermaid
sequenceDiagram
participant TS as TeamSpeak 服务器<br/>产生在线成员变化
participant SDK as Go SDK (gomobile)<br/>解析 notify* 通知
participant BRIDGE as TSBridge<br/>JNI 回调转 EventBus
participant EB as EventBus<br/>事件合并与分发
participant REDUCER as ViewModel<br/>debounce 后刷新成员
participant STORE as 成员实体仓库<br/>以 clientID 保存唯一成员
participant UI as 频道树 UI<br/>派生人数和成员预览
TS-->>SDK: notifycliententerview
SDK->>BRIDGE: OnClientEnter(Client)
BRIDGE->>EB: emit(ClientEnter)
TS-->>SDK: notifyclientmoved
SDK->>BRIDGE: OnClientMoved(id, channelID)
BRIDGE->>EB: emit(ClientMoved)
TS-->>SDK: notifyclientleftview
SDK->>BRIDGE: OnClientLeave(id, reason)
BRIDGE->>EB: emit(ClientLeave)
Note over EB: debounce 300ms 合并
EB->>REDUCER: collect → refreshClientList()
REDUCER->>STORE: TSBridge.getClientsJSON() → 全量刷新
STORE-->>UI: 实体引用变化
```
---
## 三、事件依赖
### 成员相关前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `getClientsJSON` 增量归并 | 已有频道基线更易校验 | 与 `getChannelsJSON` 同一同步批次 | 未知频道触发频道补偿读取 |
| `ClientEnter` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
| `ClientMoved` 事件 | 已注册 EventBus 事件收集 | 已有成员基线和自身 ID | debounce 后全量刷新 |
| `ClientLeave` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
### 服务端通知映射
| 服务端通知 | TSBridge 回调 → EventBus 事件 | 上层状态依赖 | 中文说明 |
| --- | --- | --- | --- |
| `notifycliententerview` | `OnClientEnter``ClientEnter` | 成员实体表、频道成员 Selector | 新成员进入视野后 debounce 合并刷新 |
| `notifyclientleftview` | `OnClientLeave``ClientLeave` | 成员实体表、离开原因 | debounce 合并刷新 |
| `notifyclientmoved` | `OnClientMoved``ClientMoved` | 成员位置、当前用户频道 | debounce 合并刷新 |
| `notifyclientpoke` | `OnPoked``Poked` | Poke 通知或成员卡 | 立即分发,展示发送者和 Poke 内容 |
### 缺失事件能力带来的依赖限制
根据 SDK 文档,当前公开事件不包含频道创建、频道更新和频道删除。因此:
1. `TsClient.getChannelList()` 是频道目录的主要权威来源。
2. 不能假设频道列表会依靠 `onClient*` 事件永久保持最新。
3. 发现成员引用未知频道时,应重新调用 `TsClient.getChannelList()`
4. 如果产品需要实时频道管理,应扩展 SDK 对相应 `notify*` 通知的支持。
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 成员实时变化 | EventBus `ClientEnter/Leave/Moved` 事件 | 服务端主动推送实时事实,debounce 后全量刷新 |
+84
View File
@@ -0,0 +1,84 @@
# 切换频道
> 用户流程:选择目标频道 → (频道有密码时输入密码) → 发送切换请求 → 等待服务端确认 → 进入目标频道
> 对应程序流程:TsClient.clientMove(selfID, targetID, password) → 等待 onClientMoved 服务端事实 → 比对 clientID → 提交新频道
---
## 一、时序:当前用户切换频道
```mermaid
sequenceDiagram
participant UI as 频道选择 UI<br/>表达用户切换意图
participant FSM as 切换状态机<br/>管理确认、请求和等待事实
participant BRIDGE as TSBridge<br/>调用 TsClient API
participant SDK as Go SDK (gomobile)<br/>发送 ClientMove 命令
participant TS as TeamSpeak 服务器<br/>验证权限、密码并执行移动
participant EVT as EventBus<br/>处理 ClientMoved 服务端事实
participant STORE as 当前用户状态<br/>保存已确认频道
UI->>FSM: ChannelJoinRequested(targetID)
FSM->>STORE: 读取 confirmedChannelID
FSM-->>UI: 显示确认或密码输入
UI->>FSM: ChannelJoinConfirmed(password)
FSM->>FSM: 状态改为 requesting
FSM->>BRIDGE: clientMove(selfID, targetID, password)
BRIDGE->>SDK: gomobile ClientMove
SDK->>TS: clientmove
alt 命令被服务器拒绝
TS-->>SDK: error
SDK-->>BRIDGE: 命令失败
BRIDGE-->>FSM: 返回错误
FSM->>FSM: 状态改为 failed
FSM-->>UI: 显示失败原因
else 命令请求成功
TS-->>SDK: command ok
SDK-->>BRIDGE: 命令成功
BRIDGE-->>FSM: 返回成功(不代表状态已提交)
FSM->>FSM: 状态改为 waitingServerEvent
TS-->>SDK: notifyclientmoved
SDK->>EVT: onClientMoved(TsClientMoved)
EVT->>STORE: 比对 clientID == selfID
EVT->>STORE: 提交 channelID = targetID
STORE-->>FSM: SelfMoved(targetID)
FSM->>FSM: 状态恢复 idle
FSM-->>UI: 导航到目标房间
end
```
---
## 二、事件依赖
### 频道切换前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `TsClient.clientMove` | 已连接、目标频道 ID、目标客户端 ID | 已同步频道和成员 | 命令失败保留原频道事实 |
| 提交自己新频道 | 自己的 `onClientMoved` | 匹配目标频道和请求上下文 | 命令响应不能直接提交频道 |
### 命令响应与事件事实的区别
```mermaid
flowchart LR
INTENT[用户意图<br/>例如发送消息或切换频道] --> COMMAND[客户端请求<br/>调用 SDK 命令方法]
COMMAND --> RESPONSE{命令响应<br/>判断服务器是否接受请求}
RESPONSE -- 失败 --> FAILED[请求失败<br/>保留原事实并展示原因]
RESPONSE -- 成功 --> WAITFACT[等待事实事件<br/>仅对存在对应推送的操作适用]
WAITFACT --> EVENT[服务端推送事件<br/>例如 OnClientMoved 或 OnTextMessage]
EVENT --> COMMIT[提交服务器事实<br/>更新频道位置或消息实体]
RESPONSE -- 无对应事实事件 --> LOCALDONE[完成本次调用<br/>仅记录调用结果而不猜测远端效果]
```
**关键区分:**
- `TsClient.clientMove` 返回成功:命令执行成功;当前用户频道应由自己的 `onClientMoved` 最终确认。
- 命令成功 ≠ 状态已提交。必须等待服务端推送的 `onClientMoved` 事件才能更新本地频道事实。
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 当前用户频道 | 自己的 `onClientMoved` 事件 | 命令响应只表示服务器接受请求,不表示状态已生效 |
+99
View File
@@ -0,0 +1,99 @@
# 文本消息
> 用户流程:收到文本消息 → 发送文本消息 → 消息按目标归档
> 对应程序流程:TsClient.sendTextMessage(targetMode, targetID, text) → 等待命令响应 → onTextMessage 按 targetMode/target 归档
---
## 一、状态树
### 聊天状态
```mermaid
flowchart TD
CHAT[聊天状态<br/>维护发送与接收消息]
CHAT --> T0[空闲 Idle<br/>没有正在发送的文本消息]
CHAT --> T1[发送中 Sending<br/>sendTextMessage 等待响应]
CHAT --> T2[发送失败 Failed<br/>命令返回错误]
CHAT --> T3[收到消息 Received<br/>OnTextMessage 归档服务端推送]
```
---
## 二、时序:发送与接收文本消息
```mermaid
sequenceDiagram
participant UI as 聊天 UI<br/>输入消息并展示会话
participant CHAT as 聊天状态机<br/>管理发送 Pending 和失败
participant BRIDGE as TSBridge<br/>调用 TsClient API
participant SDK as Go SDK (gomobile)<br/>发送并解析文本消息
participant TS as TeamSpeak 服务器<br/>路由私聊、频道或服务器消息
participant STORE as 消息仓库<br/>按真实目标会话归档
UI->>CHAT: MessageSendRequested(targetMode, targetID, text)
CHAT->>CHAT: 创建 requestId 与 sending 状态
CHAT->>BRIDGE: sendTextMessage(targetMode, targetID, text)
BRIDGE->>SDK: gomobile SendTextMessage
SDK->>TS: sendtextmessage
alt 发送失败
TS-->>SDK: error
SDK-->>BRIDGE: 命令失败
BRIDGE-->>CHAT: 返回错误
CHAT-->>UI: 标记消息发送失败
else 发送成功
TS-->>SDK: command ok
SDK-->>BRIDGE: 命令成功
BRIDGE-->>CHAT: 返回成功
CHAT-->>UI: 标记发送完成
end
TS-->>SDK: notifytextmessage
SDK->>CHAT: onTextMessage(TsTextMessage)
CHAT->>STORE: 按 targetMode 与 target 归档
STORE-->>UI: 更新目标会话和未读状态
```
---
## 三、事件依赖
### 文本消息前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `TsClient.sendTextMessage` | 已连接、合法 targetMode 和 target | 已建立目标会话上下文 | 失败保留可重试消息状态 |
| `onTextMessage` 归档 | `targetMode``target`、发送者信息 | 已有频道或私聊实体 | 不得使用当前页面猜测目标 |
### 服务端通知映射
| 服务端通知 | TSBridge 回调 → EventBus 事件 | 上层状态依赖 | 中文说明 |
| --- | --- | --- | --- |
| `notifytextmessage` | `OnTextMessage``TextMessage` | 消息归档、未读 | 立即分发,必须读取 targetMode 和 target |
### 命令响应与事件事实的区别
```mermaid
flowchart LR
INTENT[用户意图<br/>发送文本消息] --> COMMAND[客户端请求<br/>SendTextMessage]
COMMAND --> RESPONSE{命令响应<br/>判断服务器是否接受请求}
RESPONSE -- 失败 --> FAILED[请求失败<br/>保留原事实并展示原因]
RESPONSE -- 成功 --> WAITFACT[等待事实事件<br/>适用于有对应推送的操作]
WAITFACT --> EVENT[服务端推送事件<br/>OnTextMessage]
EVENT --> COMMIT[提交服务器事实<br/>归档消息实体]
RESPONSE -- 无对应事实事件 --> LOCALDONE[完成本次调用<br/>仅记录调用结果]
```
**关键区分:**
- `TsClient.sendTextMessage` 返回成功:发送命令成功;如果服务器会向自己回推消息,可再用 `onTextMessage` 归档权威消息。
- 消息归档必须依据 `targetMode``target`,不能使用当前页面频道代替真实目标。
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 文本消息 | `onTextMessage` | 来源是 `notifytextmessage` |
+81
View File
@@ -0,0 +1,81 @@
# 语音通信
> 用户流程:按下 PTT 按钮 → 发送语音 → 松开 PTT 按钮 → 停止语音
> 对应程序流程:前置条件检查 → StartCapture → 循环 TsClient.sendVoice(clientID, codec, frame) → StopCapture
---
## 一、状态树
### 语音状态
```mermaid
flowchart TD
VOICE[语音状态<br/>维护 Opus 帧发送过程]
VOICE --> V0[静默 Silent<br/>没有发送语音帧]
VOICE --> V1[采集中 Capturing<br/>上层正在生成 Opus 帧]
VOICE --> V2[发送中 Sending<br/>循环调用 sendVoice]
VOICE --> V3[受阻 Blocked<br/>未连接、采集失败或发送异常]
```
---
## 二、时序:发送语音帧与异常停止
```mermaid
sequenceDiagram
participant UI as PTT 控件<br/>接收按下、松开和取消操作
participant VFSM as 语音发送状态机<br/>检查连接与频道前置条件
participant CAP as 音频采集与编码器<br/>生成原始 Opus 帧
participant BRIDGE as TSBridge<br/>调用 TsClient API
participant SDK as Go SDK (gomobile)<br/>通过 SendVoice 发送语音
participant TS as TeamSpeak 服务器<br/>转发语音给频道成员
UI->>VFSM: PttPressed
VFSM->>VFSM: 检查 connected 和 joinedChannel
alt 前置条件不满足
VFSM-->>UI: Blocked(reason)
else 前置条件满足
VFSM->>CAP: StartCapture()
CAP-->>VFSM: CaptureReady
VFSM->>VFSM: 状态改为 transmitting
loop 每个 Opus 帧
CAP-->>VFSM: opusFrame
VFSM->>BRIDGE: sendVoice(clientID, codec, frame)
BRIDGE->>SDK: gomobile SendVoice
SDK->>TS: UDP voice frame
end
alt 用户松开或取消
UI->>VFSM: PttReleased / PointerCancelled
VFSM->>CAP: StopCapture()
VFSM->>VFSM: 状态恢复 silent
else 连接断开
SDK-->>VFSM: onDisconnected(error)
VFSM->>CAP: StopCapture()
VFSM->>VFSM: 状态改为 blocked
end
end
```
---
## 三、事件依赖
### 语音前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `TsClient.sendVoice` | 已连接、已有可发送的 Opus 帧 | 当前用户已在有效频道 | 任一前置失效立即停止发送 |
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 语音发送结果 | `TsClient.sendVoice` 返回值 | 表示本次本地发送调用结果,不代表对方播放成功 |
### 统一实现原则(语音相关)
- **语音只在连接和频道前置条件满足时发送。** 断开或被踢必须立即停止。
+108
View File
@@ -0,0 +1,108 @@
# 文件传输
> 用户流程:选择文件 → 上传/下载 → 等待传输完成
> 对应程序流程:FileTransferInit → DialFileTransfer → UploadFileData / DownloadFileData
> 注:文件传输 API 暂未在 `TsClient` 中封装,需通过 `TSBridge` 直接调用 gomobile 对象
---
## 一、状态树
### 文件状态
```mermaid
flowchart TD
FILE[文件状态<br/>维护上传和下载过程]
FILE --> F0[空闲 Idle<br/>没有文件传输任务]
FILE --> F1[初始化 Initializing<br/>请求 FileTransferInitUpload 或 Download]
FILE --> F2[连接中 Dialing<br/>DialFileTransfer 建立 TCP 连接]
FILE --> F3[传输中 Transferring<br/>UploadFileData 或 DownloadFileData 搬运数据]
FILE --> F4[完成 Completed<br/>文件数据传输结束]
FILE --> F5[失败 Failed<br/>初始化、连接或传输发生错误]
```
---
## 二、时序:文件上传与下载
```mermaid
sequenceDiagram
participant UI as 文件操作 UI<br/>选择上传或下载任务
participant SDK as teamspeak-go SDK<br/>初始化文件传输并提供辅助函数
participant TS as TeamSpeak 服务器<br/>分配文件传输连接信息
participant TCP as 文件 TCP 连接<br/>承载实际文件字节流
participant IO as 本地 Reader/Writer<br/>提供上传源或接收下载结果
alt 上传文件
UI->>SDK: FileTransferInitUpload(channelID, path, password, size, overwrite)<br/>中文注释:请求初始化频道文件上传
SDK->>TS: ftinitupload<br/>中文注释:提交目标路径、大小和覆盖选项
TS-->>SDK: FileUploadInfo<br/>中文注释:返回 host、port 和传输 key
SDK-->>UI: uploadInfo<br/>中文注释:初始化成功后才能建立数据连接
UI->>SDK: DialFileTransfer(host, port, key)<br/>中文注释:建立独立 TCP 文件连接
SDK->>TCP: TCP connect<br/>中文注释:连接服务器文件传输端口
UI->>SDK: UploadFileData(host, info, reader)<br/>中文注释:从本地 Reader 读取上传内容
IO-->>SDK: 文件字节流<br/>中文注释:持续提供待上传数据
SDK->>TCP: 上传字节流<br/>中文注释:通过 TCP 发送文件内容
TCP->>TS: 完成上传<br/>中文注释:服务器保存频道文件
else 下载文件
UI->>SDK: FileTransferInitDownload(channelID, path, password)<br/>中文注释:请求初始化频道文件下载
SDK->>TS: ftinitdownload<br/>中文注释:提交目标频道和文件路径
TS-->>SDK: FileDownloadInfo<br/>中文注释:返回 host、port 和传输 key
SDK-->>UI: downloadInfo<br/>中文注释:初始化成功后才能建立数据连接
UI->>SDK: DialFileTransfer(host, port, key)<br/>中文注释:建立独立 TCP 文件连接
SDK->>TCP: TCP connect<br/>中文注释:连接服务器文件传输端口
UI->>SDK: DownloadFileData(host, info, writer)<br/>中文注释:指定本地 Writer 接收文件
TS->>TCP: 下载字节流<br/>中文注释:服务器持续发送文件内容
TCP->>SDK: 文件字节流<br/>中文注释:SDK 从 tcp 读取下载数据
SDK->>IO: 写入本地 Writer<br/>中文注释:保存或消费下载内容
end
```
---
## 三、事件依赖
### 文件传输前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `DialFileTransfer` | 上传或下载初始化结果 | 初始化信息尚未过期 | 不得跳过初始化直接拨号 |
| `UploadFileData` | 上传信息、Reader、有效 TCP 条件 | 文件大小与声明一致 | 中止任务并展示传输错误 |
| `DownloadFileData` | 下载信息、Writer、有效 TCP 条件 | 本地空间和写权限可用 | 中止任务并清理不完整结果 |
### 事件依赖图(文件传输)
```mermaid
flowchart TD
READY --> FTINIT[文件传输初始化<br/>请求上传或下载连接信息]
FTINIT --> FTDIAL[DialFileTransfer<br/>依赖 host、port 和 key 建立 TCP]
FTDIAL --> FTDATA[上传或下载数据<br/>依赖已初始化的文件连接]
```
### 命令响应与事件事实的区别
```mermaid
flowchart LR
INTENT[用户意图<br/>上传或下载文件] --> COMMAND[客户端请求<br/>FileTransferInit]
COMMAND --> RESPONSE{命令响应<br/>判断服务器是否接受请求}
RESPONSE -- 失败 --> FAILED[请求失败<br/>展示错误原因]
RESPONSE -- 成功 --> INFO[获得传输参数<br/>host、port、key]
INFO --> DIAL[建立 TCP 连接<br/>DialFileTransfer]
DIAL --> TRANSFER[数据传输<br/>Upload/DownloadFileData]
```
**关键区分:**
- `FileTransferInitUpload/Download` 成功:只获得传输参数;文件内容仍依赖后续 TCP 连接和数据传输。
- 文件传输严格遵守"初始化 → TCP 连接 → 数据传输"三个阶段,不能跳过。
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 文件传输进度 | SDK 内部文件状态事件 | `notifystartupload``notifystartdownload``notifystatusfiletransfer` |
### 统一实现原则(文件传输相关)
- **文件传输严格遵守"初始化 → TCP 连接 → 数据传输"。** 三个阶段不能跳过。
+108
View File
@@ -0,0 +1,108 @@
# 断开连接
> 用户流程:主动断开 / 网络异常断开 / 被踢出 → 清理会话 → 返回主页
> 对应程序流程:TsClient.disconnect() 或 onDisconnected(error) 或 onKicked(reason) → 停止写操作 → 清理会话状态
---
## 一、状态树
### Client 总状态树(断开相关部分)
```mermaid
flowchart TD
ROOT[Client 生命周期状态树<br/>断开相关部分]
ROOT --> D[断开过程 Disconnecting<br/>正在主动或被动结束会话]
ROOT --> X[已结束 Terminated<br/>当前会话资源已清理]
D --> D1[GracefulDisconnect<br/>用户调用 TsClient.disconnect 优雅退出]
D --> D2[UnexpectedDisconnect<br/>网络或服务端导致 onDisconnected]
D --> D3[Kicked<br/>onKicked 表示自己被踢]
X --> X1[清理命令请求<br/>取消未完成的业务操作]
X --> X2[清理语音与传输<br/>停止语音和文件连接]
X --> X3[清理会话状态<br/>移除频道成员等临时事实]
```
---
## 二、时序:主动断开、异常断开与被踢
```mermaid
sequenceDiagram
participant UI as 上层应用<br/>展示连接状态并触发退出
participant VM as ViewModel<br/>收集 EventBus 事件
participant BRIDGE as TSBridge<br/>管理连接生命周期
participant SDK as Go SDK (gomobile)<br/>管理连接与断开事件
participant TS as TeamSpeak 服务器<br/>关闭或终止客户端会话
participant STORE as 会话数据仓库<br/>保存当前连接的临时事实
alt 用户主动断开
UI->>VM: DisconnectRequested
VM->>VM: 状态改为 disconnecting
VM->>BRIDGE: TSBridge.disconnect()
BRIDGE->>SDK: gomobile Disconnect
SDK->>TS: shutdown reason
TS-->>SDK: 连接关闭
SDK-->>BRIDGE: OnDisconnected 回调
BRIDGE->>BRIDGE: EventBus.emit(Disconnected)
BRIDGE->>VM: EventBus 分发 Disconnected 事件
else 网络或服务器异常
TS--xSDK: UDP 会话中断
SDK-->>BRIDGE: OnDisconnected 回调
BRIDGE->>BRIDGE: EventBus.emit(Disconnected)
BRIDGE->>VM: EventBus 分发 Disconnected 事件
else 自己被踢
TS-->>SDK: notifyclientleftview reasonid=4/5
SDK-->>BRIDGE: OnKicked 回调
BRIDGE->>BRIDGE: EventBus.emit(Kicked)
BRIDGE->>VM: EventBus 分发 Kicked 事件
end
VM->>VM: 停止语音与文件传输
VM->>STORE: 清理频道、成员和 Pending
STORE-->>UI: 返回已断开或失败页面
```
---
## 三、事件依赖
### 断开相关前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `Disconnected` 事件 | 已注册 EventBus 事件收集 | 保存断开原因 | 停止全部依赖连接的操作 |
| `Kicked` 事件 | 已注册 EventBus 事件收集 | 区分 reasonid 4 和 5 | 清理会话且不伪装为普通成员离开 |
### 事件依赖图(断开阶段)
```mermaid
flowchart TD
DISCONNECTED[EventBus Disconnected 事件<br/>接收连接终止事件] --> STOP[停止依赖操作<br/>禁止命令、语音和新文件传输]
KICKED[EventBus Kicked 事件<br/>接收自己被踢事件] --> STOP
STOP --> CLEAN[清理会话状态<br/>移除当前连接的实体与 Pending]
```
### 服务端通知映射
| 服务端通知 | TSBridge 回调 → EventBus 事件 | 上层状态依赖 | 中文说明 |
| --- | --- | --- | --- |
| `notifyclientleftview``reasonid=4/5` 且为自己 | `OnKicked``Kicked` | 当前会话、语音、导航 | SDK 将自己被踢转换为独立事件 |
### 生命周期约束(断开相关)
- `TSBridge.disconnect()` 用于主动优雅退出;`Disconnected` 事件用于观察最终断开结果或异常断开。
- `Kicked` 事件来源于 `notifyclientleftview` 的特定原因,必须与普通成员离开区分。
- 断开后应停止语音、拒绝新命令,并清理只属于当前会话的状态。
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 被踢状态 | EventBus `Kicked` 事件 | SDK 已从离开事件原因中识别自己被踢 |
### 统一实现原则(断开相关)
- **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
+318
View File
@@ -0,0 +1,318 @@
# 状态同步
> 独立于主流程之外的横切关注点:每当连接建立、事件到达、数据不一致或重新连接时,都需要更新频道、成员和服务器信息。
> 本流程不直接对应某个用户操作,而是被所有用户流程在特定步骤中触发。
> 所有 API 调用通过 `TSBridge`,事件通过 `EventBus` 接收。
---
## 一、触发时机总览
```
触发状态同步的时机
├─ ① 首次同步(连接建立后)
│ 触发源:EventBus Connected 事件
│ 动作:TSBridge.getChannelsJSON() + TSBridge.getClientsJSON() + TSBridge.getClientID()
│ 目标:建立完整的频道基线和成员基线
├─ ② 增量同步(事件驱动)
│ 触发源:EventBus ClientEnter / ClientMoved / ClientLeave 事件
│ 动作:debounce 合并后 refreshClientList() 全量刷新
│ 目标:维护实时成员位置和频道人数
├─ ③ 补偿同步(数据不一致时)
│ 触发源:事件引用了未知的 clientID 或 channelID
│ 动作:重新调用 TSBridge.getClientsJSON() 或 TSBridge.getChannelsJSON()
│ 目标:用完整列表修复丢失的基线数据
├─ ④ 自身状态同步(频道切换后)
│ 触发源:EventBus 收到自己的 ClientMoved 事件
│ 动作:比对 clientID == selfID → 更新自身所在频道
│ 目标:确认当前用户的实际频道位置
├─ ⑤ 消息归档同步(收到消息时)
│ 触发源:EventBus TextMessage 事件
│ 动作:按 targetMode + target 归档到正确的会话
│ 目标:维护消息与目标频道的对应关系
├─ ⑥ 重连后全量同步(断开重连后)
│ 触发源:重新收到 EventBus Connected 事件
│ 动作:清空旧会话状态 → 重新执行首次同步
│ 目标:确保重连后数据与服务器完全一致
└─ ⑦ 同步失败处理
触发源:TSBridge.getChannelsJSON() / TSBridge.getClientsJSON() 请求失败
动作:标记同步失败 → 不进入业务就绪 → 允许重试
目标:防止在不完整数据上执行业务操作
```
---
## 二、同步状态树
```mermaid
flowchart TD
SYNC[同步状态<br/>建立服务器数据基线]
SYNC --> S0[未同步 Unsynced<br/>已连接但尚无完整数据]
SYNC --> S1[同步中 Syncing<br/>调用 getChannelList 和 getClientList]
SYNC --> S2[已同步 Synchronized<br/>列表基线可供 UI 使用]
SYNC --> S3[同步失败 SyncFailed<br/>列表请求失败或上下文失效]
S0 --> S1
S1 --> S2
S1 --> S3
S3 --> S1
S2 --> S1
```
---
## 三、成员状态树
```mermaid
flowchart TD
MEMBER[成员状态<br/>维护在线用户及频道位置]
MEMBER --> M0[空基线 Empty<br/>尚未执行 ListClients]
MEMBER --> M1[已有基线 Loaded<br/>getClientList 已建立成员表]
MEMBER --> M2[增量更新 Updating<br/>OnClientEnter/Leave/Moved 正在归并]
MEMBER --> M3[不一致 Inconsistent<br/>事件引用未知成员或频道]
M0 --> M1
M1 --> M2
M2 --> M1
M2 --> M3
M3 --> M1
```
---
## 四、成员实体状态树
```mermaid
flowchart TD
ENTITY[成员实体状态树<br/>描述 ListClients 基线与实时事件如何协同]
ENTITY --> BASE[基线分支<br/>由客户端请求建立完整成员集合]
ENTITY --> DELTA[增量分支<br/>由服务端推送维护实时变化]
ENTITY --> SELF[当前用户分支<br/>通过 ClientID 识别自己的事件]
ENTITY --> REPAIR[修复分支<br/>处理事件缺失或引用不一致]
BASE --> B1[TSBridge.getClientsJSON 请求<br/>获取服务器当前全部在线客户端]
B1 --> B2[按 clientID 建表<br/>使用客户端 ID 去重保存]
B2 --> B3[按 channelID 建索引<br/>派生每个频道的成员列表]
DELTA --> D1[EventBus ClientEnter 事件<br/>debounce 合并后全量刷新]
DELTA --> D2[EventBus ClientMoved 事件<br/>debounce 合并后全量刷新]
DELTA --> D3[EventBus ClientLeave 事件<br/>debounce 合并后全量刷新]
SELF --> S1[TSBridge.getClientID 本地调用<br/>读取服务器分配的自身 ID]
S1 --> S2[事件 clientID 比对<br/>判断移动或离开事件是否属于自己]
S2 --> S3[更新自身频道事实<br/>自己的移动决定实际所在频道]
REPAIR --> R1[检测未知 clientID<br/>移动或离开事件找不到实体]
R1 --> R2[重新调用 TSBridge.getClientsJSON<br/>用完整列表修复成员基线]
REPAIR --> R3[检测未知 channelID<br/>成员引用本地不存在的频道]
R3 --> R4[重新调用 TSBridge.getChannelsJSON<br/>用完整列表修复频道基线]
```
---
## 五、时序:首次同步
```mermaid
sequenceDiagram
participant UI as 上层应用<br/>发起同步并更新状态
participant VM as ViewModel<br/>收集 EventBus 事件
participant BRIDGE as TSBridge<br/>调用 gomobile API
participant SDK as Go SDK (gomobile)<br/>发送列表请求
participant TS as TeamSpeak 服务器<br/>返回频道和成员数据
participant STORE as 业务状态仓库<br/>保存基线数据
Note over VM: EventBus Connected 事件触发首次同步
par 请求频道基线
VM->>BRIDGE: TSBridge.getChannelsJSON()
BRIDGE->>SDK: gomobile 调用 GetChannelsJSON
SDK->>TS: channellist
TS-->>SDK: JSON 字符串
SDK-->>BRIDGE: 频道 JSON
and 请求成员基线
VM->>BRIDGE: TSBridge.getClientsJSON()
BRIDGE->>SDK: gomobile 调用 GetClientsJSON
SDK->>TS: clientlist
TS-->>SDK: JSON 字符串
SDK-->>BRIDGE: 成员 JSON
and 读取自身 ID
VM->>BRIDGE: TSBridge.getClientID()
BRIDGE-->>VM: selfClientID
end
alt 全部成功
VM->>STORE: 原子提交频道、成员和自身 ID
STORE-->>UI: 同步完成 → 状态进入 Synchronized
else 任一失败
VM->>STORE: 标记同步失败 → 状态进入 SyncFailed
Note over UI: 允许重试,不进入业务就绪
end
```
---
## 六、时序:增量同步与补偿同步
```mermaid
sequenceDiagram
participant TS as TeamSpeak 服务器<br/>产生成员变化事件
participant SDK as Go SDK (gomobile)<br/>解析 notify* 通知
participant BRIDGE as TSBridge<br/>JNI 回调转 EventBus
participant EB as EventBus<br/>事件合并与分发
participant VM as ViewModel<br/>debounce 后刷新成员
participant STORE as 成员实体仓库<br/>以 clientID 保存唯一成员
participant UI as 频道树 UI<br/>派生人数和成员预览
Note over TS,UI: ② 增量同步 — 正常事件流
TS-->>SDK: notifycliententerview
SDK->>BRIDGE: OnClientEnter(Client)
BRIDGE->>EB: emit(ClientEnter)
TS-->>SDK: notifyclientmoved
SDK->>BRIDGE: OnClientMoved(id, channelID)
BRIDGE->>EB: emit(ClientMoved)
TS-->>SDK: notifyclientleftview
SDK->>BRIDGE: OnClientLeave(id, reason)
BRIDGE->>EB: emit(ClientLeave)
Note over EB: debounce 300ms 合并
EB->>VM: collect → refreshClientList()
VM->>STORE: TSBridge.getClientsJSON() → 全量刷新
alt 刷新成功
STORE-->>UI: 基线已更新
else 数据不一致
Note over VM: ③ 补偿同步 — 修复丢失的基线
VM->>STORE: TSBridge.getChannelsJSON() → 修复频道基线
STORE-->>UI: 基线已修复
end
```
---
## 七、时序:重连后全量同步
```mermaid
sequenceDiagram
participant UI as 上层应用<br/>处理重连流程
participant VM as ViewModel<br/>管理清理和重建
participant BRIDGE as TSBridge<br/>管理连接生命周期
participant SDK as Go SDK (gomobile)<br/>重新建立连接
participant STORE as 会话数据仓库<br/>保存会话临时事实
Note over UI: 网络恢复或手动重连
UI->>VM: 检测到需要重连
VM->>VM: 状态改为 disconnecting
VM->>STORE: 清理旧会话数据
STORE-->>VM: 清理完成
Note over VM: 重新执行连接流程
VM->>BRIDGE: TSBridge.connect(...)
BRIDGE->>SDK: gomobile Connect
SDK-->>BRIDGE: OnConnected 回调
BRIDGE->>BRIDGE: EventBus.emit(Connected)
BRIDGE->>VM: EventBus 分发 Connected 事件
Note over VM: 重新执行首次同步
VM->>BRIDGE: getChannelsJSON + getClientsJSON + getClientID
BRIDGE->>SDK: gomobile 调用
SDK-->>BRIDGE: 完整基线数据
VM->>STORE: 原子提交新基线
STORE-->>UI: 同步完成 → 恢复业务就绪
```
---
## 八、同步与主流程的关系
```mermaid
flowchart TD
subgraph 主流程触发点
A[01 连接服务器<br/>EventBus Connected]
B[02 浏览频道<br/>EventBus ClientEnter/Moved/Leave]
C[03 切换频道<br/>EventBus ClientMoved]
D[04 文本消息<br/>EventBus TextMessage]
E[07 断开连接<br/>重新连接]
end
subgraph 状态同步流程
F[① 首次同步]
G[② 增量同步]
H[③ 补偿同步]
I[④ 自身状态同步]
J[⑤ 消息归档同步]
K[⑥ 重连后全量同步]
L[⑦ 同步失败处理]
end
A --> F
B --> G
B --> H
C --> I
D --> J
E --> K
F --> L
K --> F
H --> F
```
---
## 九、事件依赖
### 同步相关前置依赖矩阵
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| 首次同步 | EventBus `Connected` 事件 | 同时读取频道、成员和自身 ID | 任一核心请求失败则不标记业务就绪 |
| `getClientsJSON` 增量归并 | 已有频道基线更易校验 | 与 `getChannelsJSON` 同一同步批次 | 未知频道触发频道补偿读取 |
| `ClientEnter` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
| `ClientMoved` 事件 | 已注册 EventBus 事件收集 | 已有成员基线和自身 ID | debounce 后全量刷新 |
| `ClientLeave` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
| 提交自己新频道 | 自己的 `ClientMoved` 事件 | 匹配目标频道和请求上下文 | 命令响应不能直接提交频道 |
| `TextMessage` 归档 | `targetMode``target`、发送者信息 | 已有频道或私聊实体 | 不得使用当前页面猜测目标 |
### 权威性划分
| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 初始频道列表 | `TSBridge.getChannelsJSON()` 响应 | SDK 未列出频道创建、更新、删除事件 |
| 初始在线成员 | `TSBridge.getClientsJSON()` 响应 | 建立完整在线成员基线 |
| 自身客户端 ID | `TSBridge.getClientID()` | SDK 本地缓存服务器分配的 ID |
| 成员实时变化 | EventBus `ClientEnter/Leave/Moved` 事件 | 服务端主动推送实时事实,debounce 后全量刷新 |
| 文本消息 | EventBus `TextMessage` 事件 | 来源是 `notifytextmessage` |
### 缺失事件能力带来的依赖限制
根据 SDK 文档,当前公开事件不包含频道创建、频道更新和频道删除。因此:
1. `TsClient.getChannelList()` 是频道目录的主要权威来源。
2. 不能假设频道列表会依靠 `onClient*` 事件永久保持最新。
3. 发现成员引用未知频道时,应重新调用 `TsClient.getChannelList()`
4. 如果产品需要实时频道管理,应扩展 SDK 对相应 `notify*` 通知的支持。
### 统一实现原则(同步相关)
- **列表命令建立基线,服务端事件维护增量。** 两者缺一不可。
- **成员表按 ClientID 幂等更新。** debounce 合并后 refreshClientList 全量刷新,天然幂等。
- **当前用户通过 ClientID 识别。** 不依赖昵称或 UI 当前页面判断自己。
- **未知实体引用触发补偿同步。** 不静默忽略移动或离开事件。
- **消息按 TargetMode 和 Target 归档。** 不使用当前页面频道代替真实目标。
- **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
- **事件通过 EventBus 分发,不在 JNI 回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`ViewModel 通过 `collect()` 响应。
+321
View File
@@ -0,0 +1,321 @@
# EventBus 架构
> 本文档描述 TeamSpeak 事件总线(EventBus)的设计,将 TS 事件线程与 UI 渲染线程分离。
> EventBus 是 TSBridge 回调与 ViewModel 状态更新之间的中间层,负责事件收集、合并、节流和分发。
---
## 一、设计目的
### 1.1 问题
当前架构中,Go JNI 回调直接触发 ViewModel 状态更新:
```
Go goroutine → JNI callback → ServerViewModel (直接写 StateFlow)
```
存在的问题:
1. **线程不一致**JNI 回调在 Go goroutine 线程上执行,部分状态更新直接赋值(`_state.value = xxx`),部分通过 `viewModelScope.launch` 切到 Main,缺乏统一的线程策略。
2. **事件风暴**:服务器连续推送多个 `onClientEnter` / `onClientMoved` 事件时,每次都触发 `refreshClientList()` 全量查询,造成不必要的命令开销和 UI 频繁重组。
3. **关注点耦合**TSBridge 回调直接依赖 ViewModel 实现,无法独立调试、日志记录或重放事件。
### 1.2 目标
```
Go goroutine → JNI callback → TSBridge → [EventBus] → ViewModel → StateFlow → UI
这一层负责:
- 统一线程调度(全部切到 Main)
- 合并高频事件(debounce
- 事件日志与调试
- TS 线程完全不碰 UI 状态
```
---
## 二、线程模型全景
```mermaid
flowchart TB
subgraph GO[Go Runtime]
NET[Network goroutines<br/>receiveLoop / processLoop / resendLoop / pingLoop]
EVT[事件消费 goroutine<br/>串行调用 JNI 回调]
NET --> EVT
end
subgraph KOTLIN[Kotlin / Android]
subgraph IO[Dispatchers.IO 线程池]
CAP[麦克风采集循环]
PLAY[语音解码与播放]
FETCH[JSON 解析<br/>fetchChannels / fetchClients]
end
subgraph MAIN[Main Thread]
VM[ViewModel 业务逻辑]
SF[StateFlow 状态更新]
UI[Compose UI 渲染]
VM --> SF --> UI
end
EB[EventBus<br/>事件收集 / 合并 / 分发]
end
EVT -->|JNI 回调<br/>Go goroutine 线程| EB
EB -->|Dispatchers.Main| VM
CAP -->|Dispatchers.IO| EB
FETCH -->|Dispatchers.IO| EB
```
### 线程职责划分
| 线程 / 调度器 | 职责 | 说明 |
| --- | --- | --- |
| Go goroutines | 网络收发、协议处理、事件队列消费 | 双层事件队列保证 JNI 回调串行执行 |
| JNI 回调线程 | 仅负责"把事件扔进 EventBus" | 不做任何状态更新或 UI 操作 |
| Dispatchers.IO | 麦克风采集、语音播放、JSON 解析 | 阻塞 IO 操作 |
| Dispatchers.Main | ViewModel 逻辑、StateFlow 更新、Compose 渲染 | 所有 UI 状态变更在主线程 |
---
## 三、事件定义
### 3.1 TSEvent 密封类
```kotlin
sealed class TSEvent {
// 连接生命周期
object Connected : TSEvent()
data class Disconnected(val message: String) : TSEvent()
data class Kicked(val reason: String) : TSEvent()
// 成员变化
data class ClientEnter(val client: Client) : TSEvent()
data class ClientLeave(val id: Long, val reasonMsg: String) : TSEvent()
data class ClientMoved(val id: Long, val targetChannelID: String) : TSEvent()
// 消息
data class TextMessage(val msg: TextMsg) : TSEvent()
// 语音
data class VoiceData(val clientID: Long, val data: ByteArray, val codec: Long) : TSEvent()
// 通知
data class Poked(val event: TSBridge.PokeEventData) : TSEvent()
}
```
### 3.2 事件分类
| 类别 | 事件 | 处理策略 |
| --- | --- | --- |
| 连接生命周期 | Connected / Disconnected / Kicked | **立即分发** — 影响全局状态,不可延迟 |
| 成员变化 | ClientEnter / ClientLeave / ClientMoved | **合并分发** — debounce 后触发一次 refreshClientList |
| 消息 | TextMessage | **立即分发** — 用户期望实时看到新消息 |
| 语音 | VoiceData | **IO 线程直接处理** — 不经过 EventBus,延迟敏感 |
| 通知 | Poked | **立即分发** — 需要弹出通知 |
---
## 四、EventBus 实现
### 4.1 核心结构
```kotlin
object EventBus {
private val _events = MutableSharedFlow<TSEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<TSEvent> = _events.asSharedFlow()
/** 发送事件(可从任意线程调用) */
fun emit(event: TSEvent) {
_events.tryEmit(event)
}
}
```
### 4.2 事件合并策略
对于高频成员变化事件,使用 debounce 合并:
```kotlin
// 在 ViewModel 中收集事件时
viewModelScope.launch {
EventBus.events
.filterIsInstance<TSEvent.ClientEnter>()
.debounce(300) // 300ms 内的多次 ClientEnter 合并为一次
.collect {
Repository.refreshClientList()
}
}
```
合并规则:
| 事件 | 合并策略 | 延迟 | 说明 |
| --- | --- | --- | --- |
| ClientEnter | debounce | 300ms | 连续多人进入时只刷新一次 |
| ClientLeave | debounce | 300ms | 连续多人离开时只刷新一次 |
| ClientMoved | debounce | 300ms | 连续多人移动时只刷新一次 |
| Connected | 不合并 | 0 | 立即处理 |
| Disconnected | 不合并 | 0 | 立即处理 |
| Kicked | 不合并 | 0 | 立即处理 |
| TextMessage | 不合并 | 0 | 立即归档 |
| Poked | 不合并 | 0 | 立即弹出 |
### 4.3 TSBridge 回调注册
```kotlin
// TSBridge.connect() 内部
val callbackProxy = object : EventCallback {
override fun onConnected() {
EventBus.emit(TSEvent.Connected)
}
override fun onDisconnected(message: String) {
EventBus.emit(TSEvent.Disconnected(message))
}
override fun onTextMessage(msg: TextMsg?) {
msg?.let { EventBus.emit(TSEvent.TextMessage(it)) }
}
override fun onClientEnter(client: Client?) {
client?.let { EventBus.emit(TSEvent.ClientEnter(it)) }
}
override fun onClientLeave(id: Long, reasonMsg: String) {
EventBus.emit(TSEvent.ClientLeave(id, reasonMsg))
}
override fun onClientMoved(id: Long, targetChannelID: String) {
EventBus.emit(TSEvent.ClientMoved(id, targetChannelID))
}
override fun onKicked(reason: String) {
EventBus.emit(TSEvent.Kicked(reason))
}
override fun onVoiceData(clientID: Long, data: ByteArray?, codec: Long) {
// 语音数据延迟敏感,不经过 EventBus,直接回调 VoiceService
data?.let { callbacks.onVoiceData(clientID, it, codec) }
}
override fun onPoked(event: TSBridge.PokeEventData?) {
event?.let { EventBus.emit(TSEvent.Poked(it)) }
}
}
```
---
## 五、事件流时序
### 5.1 成员变化事件流(合并)
```mermaid
sequenceDiagram
participant TS as TeamSpeak 服务器
participant GO as Go 事件消费 goroutine
participant JNI as JNI 回调
participant EB as EventBus
participant VM as ViewModel (Main)
participant UI as Compose UI
TS-->>GO: notifycliententerview (客户端 A)
GO->>JNI: OnClientEnter(A)
JNI->>EB: emit(ClientEnter(A))
TS-->>GO: notifycliententerview (客户端 B)
GO->>JNI: OnClientEnter(B)
JNI->>EB: emit(ClientEnter(B))
TS-->>GO: notifyclientmoved (客户端 C)
GO->>JNI: OnClientMoved(C)
JNI->>EB: emit(ClientMoved(C))
Note over EB: debounce 300ms 合并
EB->>VM: collect → refreshClientList()
VM->>UI: StateFlow 更新 → 一次性重组
```
### 5.2 连接事件流(立即)
```mermaid
sequenceDiagram
participant GO as Go 事件消费 goroutine
participant JNI as JNI 回调
participant EB as EventBus
participant VM as ServerViewModel (Main)
participant UI as Compose UI
GO->>JNI: OnConnected()
JNI->>EB: emit(Connected)
EB->>VM: collect → 处理连接成功
VM->>VM: 执行首次同步
VM->>UI: 连接状态更新
```
### 5.3 语音数据流(绕过 EventBus
```mermaid
sequenceDiagram
participant GO as Go 事件消费 goroutine
participant JNI as JNI 回调
participant VS as VoiceService (IO)
participant SP as 扬声器
GO->>JNI: OnVoiceData(clientID, data, codec)
JNI->>VS: handleVoiceData() 直接回调
VS->>VS: Opus 解码 (Dispatchers.IO)
VS->>SP: AudioTrack.write()
```
---
## 六、EventBus 与现有组件的关系
```mermaid
flowchart TB
subgraph 现有架构
TSB[TSBridge<br/>JNI 回调注册]
SVM[ServerViewModel<br/>连接 / 断开 / 重连]
CVM[ChannelViewModel<br/>频道列表 / 切换]
CHVM[ChatViewModel<br/>消息归档]
VVM[VoiceViewModel<br/>语音控制]
REPO[Repository<br/>状态仓库]
end
subgraph 新增
EB[EventBus<br/>事件收集 / 合并 / 分发]
EVT[TSEvent 密封类<br/>事件类型定义]
end
TSB -->|JNI 回调| EB
EB -->|SharedFlow| SVM
EB -->|SharedFlow| CVM
EB -->|SharedFlow| CHVM
EB -->|SharedFlow| VVM
SVM --> REPO
CVM --> REPO
CHVM --> REPO
EB -.->|语音绕过| VVM
```
### 各 ViewModel 监听的事件
| ViewModel | 监听事件 | 处理方式 |
| --- | --- | --- |
| ServerViewModel | Connected / Disconnected / Kicked | 立即更新连接状态 |
| ChannelViewModel | ClientEnter / ClientLeave / ClientMoved | debounce 后 refreshClientList |
| ChatViewModel | TextMessage | 立即归档消息 |
| VoiceViewModel | — | 语音数据由 VoiceService 直接处理 |
---
## 七、与统一实现原则的关系
EventBus 架构强化了以下实现原则:
1. **事件通过 EventBus 分发,不在回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`ViewModel 通过 `collect()` 响应。
2. **列表命令建立基线,服务端事件维护增量。** EventBus 的 debounce 机制确保增量事件不会导致过度刷新。
3. **成员表按 ClientID 幂等更新。** debounce 后的 refreshClientList 是全量刷新,天然幂等。
4. **断开时统一清理当前会话资源。** Disconnected 事件通过 EventBus 统一分发,各 ViewModel 统一响应清理。