首次推送

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
@@ -0,0 +1,552 @@
# 步骤 04:连接与首次同步
> 实现完整的连接流程:Identity 管理、Connect、WaitConnected、首次同步。
---
## 一、目标
- [ ] Identity 生成与持久化
- [ ] ClientOption 组装
- [ ] 事件处理器注册
- [ ] Connect + WaitConnected 流程
- [ ] 首次同步(ListChannels + ListClients + ClientID
- [ ] 连接状态与错误处理
---
## 二、任务清单
### 4.1 Identity 管理
**目标**:实现 TeamSpeak 加密身份的生成与持久化存储。
**任务**
1. **生成 Identity**
```kotlin
// Go 侧通过 Bridge 暴露生成接口
// Kotlin 侧调用生成并获取 Identity 字符串
val identity = TSBridge.generateIdentity()
```
2. **持久化存储**
- 使用 `SharedPreferences` 或 `DataStore` 存储 Identity
- Key 建议:`ts_identity`
- 首次启动时生成,后续启动时读取
3. **读取与恢复**
```kotlin
fun loadOrCreateIdentity(context: Context): String {
val prefs = context.getSharedPreferences("ts_config", Context.MODE_PRIVATE)
return prefs.getString("ts_identity", null)
?: TSBridge.generateIdentity().also {
prefs.edit().putString("ts_identity", it).apply()
}
}
```
**注意事项**
- Identity 是客户端加密身份,必须持久化,否则每次连接会被服务器视为新用户
- 生成后不可更改,丢失需重新生成(会丢失服务器端的权限关联)
### 4.2 连接流程实现
**目标**:实现完整的连接流程,从配置组装到连接成功。
**任务**
1. **连接参数数据类**
```kotlin
data class ConnectionConfig(
val address: String, // 服务器地址(IP/域名/TSDNS
val nickname: String, // 显示昵称
val password: String? = null, // 服务器密码(可选)
val defaultChannel: String? = null, // 默认频道(可选)
val defaultChannelPassword: String? = null // 默认频道密码(可选)
)
```
2. **Bridge 层连接接口封装**
```kotlin
// TSBridge.kt 中新增
fun connect(config: ConnectionConfig): Result<Unit> {
return try {
val identity = loadOrCreateIdentity(context)
// 调用 Go 侧 NewClient + Connect
tsClient.newClient(identity, config.address, config.nickname)
if (config.password != null) {
tsClient.setServerPassword(config.password)
}
if (config.defaultChannel != null) {
tsClient.setDefaultChannel(config.defaultChannel)
if (config.defaultChannelPassword != null) {
tsClient.setDefaultChannelPassword(config.defaultChannelPassword)
}
}
tsClient.connect()
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
```
3. **WaitConnected 实现**
```kotlin
suspend fun waitConnected(timeout: Duration = 30.seconds): Result<Unit> {
return withContext(Dispatchers.IO) {
try {
// Go 侧阻塞等待,支持 context 取消
tsClient.waitConnected(timeout.inWholeMilliseconds)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
```
4. **完整连接流程**
```kotlin
// ServerViewModel.kt
suspend fun connect(config: ConnectionConfig) {
_connectionState.value = ConnectionState.Connecting
// 1. 注册事件处理器(Connect 前必须完成)
registerEventHandlers()
// 2. 发起连接
val connectResult = TSBridge.connect(config)
if (connectResult.isFailure) {
_connectionState.value = ConnectionState.Failed(connectResult.exceptionOrNull()!!)
return
}
// 3. 等待连接就绪
val waitResult = TSBridge.waitConnected()
if (waitResult.isFailure) {
_connectionState.value = ConnectionState.Failed(waitResult.exceptionOrNull()!!)
return
}
// 4. 连接成功,等待 OnConnected 事件触发首次同步
}
```
**连接状态枚举**
```kotlin
sealed class ConnectionState {
object Disconnected : ConnectionState()
object Connecting : ConnectionState()
object Connected : ConnectionState() // WaitConnected 成功
object Syncing : ConnectionState() // 首次同步中
object Ready : ConnectionState() // 业务就绪
data class Failed(val error: Throwable) : ConnectionState()
}
```
**时序约束**
- `Connect()` 成功只表示连接流程已启动
- 发送业务命令前必须等待 `WaitConnected()` 成功
- 事件处理器必须在 `Connect()` 前注册,避免早期事件丢失
### 4.3 事件注册
**目标**:在 Connect 前注册所有必要的事件处理器,确保不遗漏早期服务端推送。
**任务**
1. **事件处理器注册(Go Bridge 侧)**
```go
// bridge.go 中暴露注册接口
func (b *Bridge) RegisterEventHandlers() {
b.client.OnConnected(func() {
b.notifyEvent("connected", nil)
})
b.client.OnDisconnected(func(err error) {
b.notifyEvent("disconnected", map[string]interface{}{
"error": err.Error(),
})
})
b.client.OnClientEnter(func(info ClientInfo) {
data, _ := json.Marshal(info)
b.notifyEvent("client_enter", string(data))
})
b.client.OnClientLeave(func(event ClientLeftViewEvent) {
data, _ := json.Marshal(event)
b.notifyEvent("client_leave", string(data))
})
b.client.OnClientMoved(func(event ClientMovedEvent) {
data, _ := json.Marshal(event)
b.notifyEvent("client_moved", string(data))
})
b.client.OnTextMessage(func(msg TextMessage) {
data, _ := json.Marshal(msg)
b.notifyEvent("text_message", string(data))
})
b.client.OnPoked(func(event PokeEvent) {
data, _ := json.Marshal(event)
b.notifyEvent("poked", string(data))
})
b.client.OnKicked(func(reason string) {
b.notifyEvent("kicked", reason)
})
}
```
2. **Kotlin 侧事件监听**
```kotlin
// TSBridge.kt
fun setEventListener(listener: (String, String) -> Unit) {
// 接收 Go 侧通过 JNI 回调的事件
eventCallback = listener
}
// ServerViewModel.kt
fun registerEventHandlers() {
TSBridge.setEventListener { event, data ->
when (event) {
"connected" -> handleConnected()
"disconnected" -> handleDisconnected(data)
"client_enter" -> handleClientEnter(data)
"client_leave" -> handleClientLeave(data)
"client_moved" -> handleClientMoved(data)
"text_message" -> handleTextMessage(data)
"poked" -> handlePoked(data)
"kicked" -> handleKicked(data)
}
}
}
```
3. **事件处理器职责**
| 事件 | 处理器 | 职责 |
|------|--------|------|
| `connected` | `handleConnected()` | 触发首次同步流程 |
| `disconnected` | `handleDisconnected()` | 清理会话状态,更新 UI |
| `client_enter` | `handleClientEnter()` | 增量同步:添加成员到基线 |
| `client_leave` | `handleClientLeave()` | 增量同步:从基线移除成员 |
| `client_moved` | `handleClientMoved()` | 增量同步:更新成员频道位置 |
| `text_message` | `handleTextMessage()` | 消息归档:按 TargetMode 存储 |
| `poked` | `handlePoked()` | 显示 Poke 通知 |
| `kicked` | `handleKicked()` | 处理踢出,清理状态 |
**注意事项**
- 事件处理器必须在 `Connect()` 之前注册
- 事件回调在 Go 的事件循环 goroutine 中串行执行,不要做耗时操作
- 需要通过事件队列串行化 JNI 回调,避免并发问题
### 4.4 首次同步逻辑
**目标**:连接成功后,建立完整的频道基线和成员基线。
**触发时机**:收到 `OnConnected` 事件后立即执行。
**任务**
1. **并行请求三个数据源**
```kotlin
// ServerViewModel.kt
private suspend fun performInitialSync() {
_connectionState.value = ConnectionState.Syncing
try {
// 并行请求频道列表、成员列表、自身 ID
val channelsDeferred = async { TSBridge.listChannels() }
val clientsDeferred = async { TSBridge.listClients() }
val selfIdDeferred = async { TSBridge.getClientId() }
val channels = channelsDeferred.await()
val clients = clientsDeferred.await()
val selfId = selfIdDeferred.await()
// 原子提交到状态仓库
repository.updateBaseline(
channels = channels,
clients = clients,
selfClientId = selfId
)
_connectionState.value = ConnectionState.Ready
} catch (e: Exception) {
_connectionState.value = ConnectionState.SyncFailed(e)
// 允许重试,不进入业务就绪
}
}
```
2. **数据模型定义**
```kotlin
// data/Models.kt
data class ChannelInfo(
val id: Long,
val parentId: Long,
val name: String,
val order: Long = 0,
val isPassword: Boolean = false,
val isPermanent: Boolean = false,
val maxClients: Int = -1
)
data class ClientInfo(
val id: Int,
val nickname: String,
val channelId: Long,
val uid: String,
val type: Int = 0,
val serverGroups: List<String> = emptyList()
)
```
3. **状态仓库实现**
```kotlin
// data/Repository.kt
class ChannelRepository {
private val _channels = MutableStateFlow<List<ChannelInfo>>(emptyList())
val channels: StateFlow<List<ChannelInfo>> = _channels
private val _clients = MutableStateFlow<List<ClientInfo>>(emptyList())
val clients: StateFlow<List<ClientInfo>> = _clients
private val _selfClientId = MutableStateFlow<Int?>(null)
val selfClientId: StateFlow<Int?> = _selfClientId
// 按频道 ID 索引的成员列表
private val _channelClients = MutableStateFlow<Map<Long, List<ClientInfo>>>(emptyMap())
val channelClients: StateFlow<Map<Long, List<ClientInfo>>> = _channelClients
fun updateBaseline(channels: List<ChannelInfo>, clients: List<ClientInfo>, selfClientId: Int) {
_channels.value = channels
_clients.value = clients
_selfClientId.value = selfClientId
// 建立频道-成员索引
_channelClients.value = clients.groupBy { it.channelId }
}
}
```
4. **Bridge 层查询**(通过 TsClient 封装)
```kotlin
// TSBridge.kt — 直接返回 Kotlin 友好类型,无需 JSON 解析
fun getChannelList(): List<TsChannel> = TsClient.getChannelList()
fun getClientList(): List<TsClientInfo> = TsClient.getClientList()
fun getClientId(): Long = TsClient.getClientId()
```
**同步状态机**
```kotlin
sealed class SyncState {
object Unsynced : SyncState() // 已连接但尚无完整数据
object Syncing : SyncState() // 调用 ListChannels 和 ListClients
object Synchronized : SyncState() // 列表基线可供 UI 使用
data class SyncFailed(val error: Throwable) : SyncState() // 同步失败
}
```
**原子提交原则**
- 频道列表、成员列表、自身 ID 必须全部成功才能提交
- 任一失败则不进入业务就绪状态
- 允许重试,避免在不完整数据上执行业务操作
### 4.5 错误处理与状态反馈
**目标**:实现完整的错误处理机制,确保用户能获得清晰的状态反馈。
**任务**
1. **连接错误分类**
```kotlin
sealed class ConnectionError : Exception() {
object InvalidAddress : ConnectionError() // 地址解析失败
object AuthenticationFailed : ConnectionError() // 密码错误
object ServerFull : ConnectionError() // 服务器满员
object Banned : ConnectionError() // 被封禁
object NetworkError : ConnectionError() // 网络问题
object Timeout : ConnectionError() // 连接超时
data class Other(val message: String) : ConnectionError()
}
```
2. **错误映射与处理**
```kotlin
fun mapConnectionError(error: Exception): ConnectionError {
val message = error.message?.lowercase() ?: ""
return when {
message.contains("resolve") || message.contains("address") ->
ConnectionError.InvalidAddress
message.contains("password") || message.contains("auth") ->
ConnectionError.AuthenticationFailed
message.contains("full") || message.contains("limit") ->
ConnectionError.ServerFull
message.contains("ban") ->
ConnectionError.Banned
message.contains("timeout") ->
ConnectionError.Timeout
message.contains("network") || message.contains("connection") ->
ConnectionError.NetworkError
else -> ConnectionError.Other(error.message ?: "Unknown error")
}
}
```
3. **状态反馈 UI**
```kotlin
@Composable
fun ConnectionStatusIndicator(state: ConnectionState) {
when (state) {
ConnectionState.Disconnected -> {
Text("未连接", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
ConnectionState.Connecting -> {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
Text("正在连接...")
}
ConnectionState.Connected -> {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
Text("已连接,正在同步...")
}
ConnectionState.Syncing -> {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
Text("正在同步数据...")
}
ConnectionState.Ready -> {
Icon(Icons.Default.CheckCircle, tint = Color.Green)
Text("就绪")
}
is ConnectionState.Failed -> {
Icon(Icons.Default.Error, tint = Color.Red)
Text("连接失败: ${state.error.getLocalizedMessage()}",
color = MaterialTheme.colorScheme.error)
Button(onClick = { /* 重试 */ }) {
Text("重试")
}
}
}
}
```
4. **同步失败重试机制**
```kotlin
// ServerViewModel.kt
private suspend fun performInitialSyncWithRetry(maxRetries: Int = 3) {
var retryCount = 0
while (retryCount < maxRetries) {
try {
performInitialSync()
return // 成功则退出
} catch (e: Exception) {
retryCount++
if (retryCount >= maxRetries) {
_connectionState.value = ConnectionState.SyncFailed(e)
return
}
// 等待后重试
delay(1000L * retryCount)
}
}
}
```
5. **断开连接清理**
```kotlin
fun disconnect() {
// 1. 停止语音(如有)
voiceViewModel.stopVoice()
// 2. 清理会话状态
repository.clearSession()
// 3. 调用 SDK 断开
TSBridge.disconnect()
// 4. 更新状态
_connectionState.value = ConnectionState.Disconnected
}
```
**错误日志记录**
```kotlin
private fun logConnectionError(error: ConnectionError) {
Log.e(TAG, "Connection error: ${error::class.simpleName}", error)
// 可选:上报到崩溃分析服务
}
```
---
## 三、验收标准
### 功能验收
- [ ] **Identity 持久化**
- 首次启动自动生成 Identity 并存储
- 后续启动读取已有 Identity,不重复生成
- 清除应用数据后能重新生成
- [ ] **连接流程**
- 输入有效地址、昵称后能成功连接服务器
- 输入错误密码时显示明确错误提示
- 连接超时时(30秒)显示超时错误
- 无网络时显示网络错误
- [ ] **首次同步**
- 连接成功后自动执行首次同步
- 频道列表正确显示(包含所有频道)
- 成员列表正确显示(包含所有在线用户)
- 自己的客户端 ID 正确识别
- [ ] **状态反馈**
- 连接过程中显示加载状态
- 同步过程中显示同步状态
- 就绪后显示就绪状态
- 错误时显示错误信息和重试按钮
- [ ] **断开连接**
- 主动断开后状态正确重置
- 被动断开(网络中断)能检测并提示
- 被踢出时显示踢出原因
### 性能验收
- [ ] 首次同步在 3 秒内完成(标准服务器,< 100 频道,< 500 用户)
- [ ] 连接建立时间 < 5 秒(正常网络环境)
### 代码质量验收
- [ ] 所有网络操作在 IO 线程执行
- [ ] 事件回调通过事件队列串行化
- [ ] 无内存泄漏(正确取消协程)
- [ ] 错误处理覆盖所有已知异常场景
### 测试用例
| 场景 | 输入 | 预期结果 |
|------|------|----------|
| 正常连接 | 有效地址、昵称、无密码 | 连接成功,频道列表显示 |
| 密码保护服务器 | 有效地址、昵称、正确密码 | 连接成功 |
| 错误密码 | 有效地址、昵称、错误密码 | 显示"密码错误"提示 |
| 无效地址 | 无效地址 | 显示"地址解析失败"提示 |
| 网络断开 | 断开网络后连接 | 显示"网络错误"提示 |
| 服务器满员 | 满员服务器 | 显示"服务器已满"提示 |
| 被封禁 | 被封禁的 UID | 显示"已被封禁"提示 |
| 连接超时 | 阻断 UDP 30 秒 | 显示"连接超时"提示 |
| 断开重连 | 断开后重新连接 | 状态正确重置,可重新连接 |
---
## 四、参考文档
- `docs/流程/01_连接服务器.md` - 完整生命周期、时序图
- `docs/流程/08_状态同步.md` - ① 首次同步
- `docs/sdk文档-go.md` - 连接管理 API