# 步骤 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
