Files
2026-07-20 19:01:03 +08:00

453 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 流程文档总览
> 本文档按用户流程组织,将 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 合并,避免事件风暴。