首次推送

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 合并,避免事件风暴。