# 步骤 02:Bridge 层实现(✅ 已完成) > 实现 Go ↔ Kotlin 桥接层,包括 Go 侧 TSClient 导出、Kotlin 友好封装(TsClient.kt + TsModels.kt)、应用层桥接(TSBridge.kt)。 --- ## 一、目标 - [x] Go 侧 TSClient 结构体及方法导出(`go/teamspeak/bridge.go`) - [x] gomobile 编译为 AAR(`android/app/libs/teamspeak.aar`) - [x] Kotlin 友好封装层(`TsClient.kt` + `TsModels.kt`) - [x] 应用层桥接(`TSBridge.kt`) --- ## 二、架构总览 ``` ┌─────────────────────────────────────────────────────────────┐ │ Kotlin 应用层 │ │ │ │ ViewModel ──→ TSBridge (object) ──→ TsClient (object) │ │ │ │ │ │ │ TsListener 回调 │ Kotlin 友好 API │ │ ↑ │ │ │ └── TsListener ─────────┘ │ │ │ ├─────────────────────────────────────────────────────────────┤ │ gomobile 自动生成层 │ │ │ │ teamspeak.TSClient (Java) ← Go TSClient struct │ │ teamspeak.EventCallback (Java) ← Go EventCallback interface│ │ teamspeak.* 数据类 (Java) ← Go 导出结构体 │ │ │ ├─────────────────────────────────────────────────────────────┤ │ Go 层 │ │ │ │ bridge.go (TSClient + EventCallback) ──→ teamspeak-go SDK │ │ │ │ │ │ │ 事件队列 + 单消费者 goroutine │ On* 回调 │ │ └──→ JNI 回调 ──→ EventCallback │ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` **数据流**: | 方向 | 路径 | 说明 | | --- | --- | --- | | Kotlin → Go | `TSBridge` → `TsClient` → gomobile `TSClient` → Go `Client` | 用户操作 | | Go → Kotlin | Go `On*` → 事件队列 → JNI → `EventCallback` → `TsClient` 转换 → `TsListener` → `TSBridge` | 事件推送 | --- ## 三、三层架构详解 ### 3.1 Go 层(bridge.go) **文件**:`go/teamspeak/bridge.go` Go 侧 Bridge 是 gomobile 导出的核心,将 teamspeak-go SDK 封装为可供 Kotlin 调用的 `TSClient` 类。 #### 导出的接口与结构 ```go // EventCallback — gomobile 导出的回调接口 type EventCallback interface { OnConnected() OnDisconnected(message string) OnTextMessage(msg *TextMsg) OnClientEnter(client *ServerClientView) OnClientLeave(client *ServerClientView) OnClientMoved(client *ClientMoved) OnKicked(reason *ServerError) OnTalkStatusChanged(talker *TalkStatusChange) OnClientIDsDone() OnServerError(error *ServerError) } // TSClient — gomobile 导出的桥接客户端 type TSClient struct { client *teamspeak.Client callback EventCallback } // 导出的数据结构(适配 gomobile 限制) type ServerClientView struct { ... } // 频道成员视图 type ClientInfo struct { ... } // 完整客户端信息 type ChannelListItem struct { ... } // 频道列表项 type TextMsg struct { ... } // 文本消息 type ClientMoved struct { ... } // 客户端移动事件 type ServerError struct { ... } // 服务器错误 type TalkStatusChange struct { ... } // 说话状态变化 type ChannelListResult struct { ... } // 频道列表结果(含数组) ``` #### 关键设计决策 1. **字符串传递 ID**:gomobile 不支持 `uint64` → Kotlin Long 的安全映射,统一用 `string` 2. **结构体包装**:gomobile 不支持 `[]*T`,用 `ChannelListResult` 包装 `ChannelListItem[]` 3. **错误用字符串**:gomobile 不支持 `error` 返回值,用空串=成功 4. **事件队列**:SDK 内部 `evtQueue` + 单消费者 goroutine 串行分发,JNI 回调线程安全 ### 3.2 Kotlin 友好封装层(TsClient.kt + TsModels.kt) **文件**:`android/app/src/main/java/com/tsmobile/app/TsClient.kt`、`TsModels.kt` 这一层将 gomobile 生成的 Java 对象转为 Kotlin 友好接口,解决: - 所有 API 为 getter/setter 而非属性 - `ChannelListItem[]` 需要手动转 `List` - 事件回调参数是 `Object` 类型需要强转 - group 类型是 `Int` 需要映射为 `ChanGroupType` 枚举 #### TsClient 对象 ```kotlin object TsClient { // 连接 fun connect(identity, host, nickname, password, defaultChannel): Boolean fun disconnect() // 事件监听 fun setListener(listener: TsListener?) // 查询(返回 Kotlin 友好类型) fun getChannelList(): List fun getClientList(): List fun getClientId(): Long fun getClientInfo(clientId: Long): TsClientInfo? fun getChannelInfo(channelId: Long): TsChannel? fun getSelf(): TsSelf fun getChannelIdsByUid(uid: String, maxDepth: Int): List // 属性 val serverVersion: String val serverIp: String val serverPlatform: String val serverName: String val serverCreated: Long val serverUptime: Long val maxClients: Int val clientsOnline: Int val channelsOnline: Int // 操作 fun sendTextMessage(targetMode: Int, targetId: Long, msg: String): Boolean fun clientMove(clientId: Long, channelId: Long, password: String): Boolean fun clientPoke(clientId: Long, msg: String): Boolean fun kickClient(clientId: Long, reasonId: Int, reasonMsg: String): Boolean fun banClient(uid: String, timeInSeconds: Long, reasonMsg: String): Boolean fun channelCreate(name: String, properties: Map, permissions: List): Long fun channelUpdate(channelId: Long, properties: Map): Boolean fun channelDelete(channelId: Long, force: Boolean): Boolean fun sendVoice(clientId: Long, codec: Int, data: ByteArray): Boolean } ``` #### TsListener 接口 ```kotlin interface TsListener { fun onConnected() fun onDisconnected(error: String?) fun onClientEnter(client: TsClientInfo) fun onClientMoved(moved: TsClientMoved) fun onClientLeave(client: ServerClientView) fun onTalkStatusChanged(talker: TsTalker) fun onClientIDsDone() fun onTextMessage(msg: TsTextMessage) fun onPoked(msg: TsTextMessage) fun onKicked(reason: ServerError?) fun onServerError(error: ServerError) fun onChannelListChanged() } ``` #### TsModels 数据类 ```kotlin // 频道 data class TsChannel(val channelListItem: ChannelListItem) { val channelId get() = channelListItem.getChannelID() val parentId get() = channelListItem.getParentChannelID() val name get() = channelListItem.getName() val order get() = channelListItem.getOrder() val isPermanent get() = channelListItem.getIsPermanent() val totalClients get() = channelListItem.getTotalClients() // ... 更多属性 } // 客户端 data class TsClientInfo(val serverClientView: ServerClientView) { val clientId get() = serverClientView.getID() val channelId get() = serverClientView.getChannelID() val nickname get() = serverClientView.getNickname() val uid get() = serverClientView.getUid() val isTalker get() = serverClientView.getIsTalker() // ... 更多属性 } // 自身信息(可写) class TsSelf internal constructor( private val view: ServerClientView, private val info: ClientInfo? ) { var nickname get() = view.getNickname() set(value) { info?.setNickname(value) } var isTalker get() = view.getIsTalker() set(value) { info?.setIsTalker(value) } var inputMuted get() = info?.getInputMuted() ?: false set(value) { info?.setInputMuted(value) } var outputMuted get() = info?.getOutputMuted() ?: false set(value) { info?.setOutputMuted(value) } // ... 更多可写属性 } // 移动事件 data class TsClientMoved( val theClient: ServerClientView?, val oldChannel: ChannelListItem?, val newChannel: ChannelListItem?, val visibility: Int ) // 说话状态 data class TsTalker( val client: ServerClientView?, val isTalking: Boolean ) // 文本消息 data class TsTextMessage( val message: String, val invokerUid: String, val invokerName: String, val invokerId: Long, val targetMode: Int, val targetClientId: Long, val targetChannelId: Long ) // 频道组/服务器组类型 enum class ChanGroupType(val value: Int) { ... } enum class ChannelGroupType(val value: Int) { ... } ``` ### 3.3 应用层桥接(TSBridge.kt) **文件**:`android/app/src/main/java/com/tsmobile/app/TSBridge.kt` TSBridge 是 ViewModel 层访问 Go 桥接的唯一入口,采用 `object` 单例模式。 #### 核心职责 1. **连接管理**:封装 `TsClient.connect()` / `disconnect()` 2. **监听注册**:在 `connect()` 时自动设置 `TsListener` 3. **查询转发**:所有查询方法委托给 `TsClient` 4. **操作转发**:所有操作方法委托给 `TsClient` #### 完整 API ```kotlin object TSBridge { // === 连接管理 === fun connect(identity: Any, host: String, nickname: String, password: String = "", defaultChannel: String = "", listener: TSBridgeListener? = null): Boolean fun disconnect() // === 查询 === fun getChannelList(): List fun getClientList(): List fun getClientId(): Long fun getSelf(): TsSelf fun getChannelInfo(channelId: Long): TsChannel? fun getClientInfo(clientId: Long): TsClientInfo? fun getChannelIdsByUid(uid: String): List // === 操作 === fun sendTextMessage(targetMode: Int, targetId: Long, msg: String): Boolean fun clientMove(clientId: Long, channelId: Long, password: String = ""): Boolean fun clientPoke(clientId: Long, msg: String): Boolean fun kickClient(clientId: Long, reasonId: Int, reasonMsg: String): Boolean fun banClient(uid: String, timeInSeconds: Long, reasonMsg: String): Boolean fun channelCreate(name: String, properties: Map = emptyMap(), permissions: List = emptyList()): Long fun channelUpdate(channelId: Long, properties: Map): Boolean fun channelDelete(channelId: Long, force: Boolean = false): Boolean fun sendVoice(clientId: Long, codec: Int, data: ByteArray): Boolean // === 服务器属性 === val serverVersion: String val serverIp: String val serverPlatform: String val serverName: String // ... } // 回调接口(简化版,供 ViewModel 使用) interface TSBridgeListener { fun onConnected() fun onDisconnected(error: String?) fun onClientEnter(client: TsClientInfo) fun onClientMoved(moved: TsClientMoved) fun onClientLeave(client: ServerClientView) fun onTalkStatusChanged(talker: TsTalker) fun onClientIDsDone() fun onTextMessage(msg: TsTextMessage) fun onPoked(msg: TsTextMessage) fun onKicked(reason: ServerError?) fun onServerError(error: ServerError) } ``` --- ## 四、数据流转详解 ### 4.1 连接流程 ``` ViewModel: TSBridge.connect(identity, host, nickname, password) ↓ TSBridge: TsClient.connect(identity, host, nickname, password) ↓ TsClient: TSClient.connect(...) // gomobile Java 对象 ↓ Go: TSClient.Connect() → NewClient → registerHandlers → Connect → WaitConnected ↓ Go SDK: 收到 welcome → 触发 OnConnected ↓ Go Bridge: EventCallback.OnConnected() // JNI ↓ TsClient: listener?.onConnected() // Kotlin 友好回调 ↓ TSBridge: listener?.onConnected() ↓ ViewModel: _state.update { it.copy(connected = true) } ``` ### 4.2 查询流程 ``` ViewModel: TSBridge.getChannelList() ↓ TSBridge: TsClient.getChannelList() ↓ TsClient: TSClient.requestChannelList() // gomobile ↓ TsClient: result.getChannels().map { TsChannel(it) } // 转为 Kotlin 类型 ↓ TSBridge: 返回 List ↓ ViewModel: _state.update { it.copy(channels = list) } ``` ### 4.3 事件推送流程 ``` TeamSpeak 服务器: notifyclientmoved ↓ Go SDK: handleNotification → evtQueue → startEventLoop ↓ Go Bridge: EventCallback.OnClientMoved(clientMoved) ↓ JNI TsClient: TsListener.onClientMoved(TsClientMoved(view, old, new, vis)) ↓ TSBridge: listener?.onClientMoved(moved) ↓ ViewModel: 处理移动事件,更新状态 ``` --- ## 五、gomobile 约束与应对 | 约束 | 影响 | 应对方案 | | --- | --- | --- | | 不能导出 `[]string` | 服务器组列表无法直接传递 | `TsClientInfo` 封装为逗号分隔字符串 | | 不能导出 `[]*T` | 频道/客户端列表无法返回切片 | `ChannelListResult` 包装 + `TsClient` 转 `List` | | 不能导出 `map[string]string` | 属性列表无法传递 | `channelCreate`/`channelUpdate` 接受 `Map`,内部转 gomobile 类型 | | 不能导出 `error` | 方法无法返回错误 | `Boolean` 返回值(true=成功) | | `uint64` 映射为 `long` | 频道 ID 可能溢出 | 统一用 `string` 传递 ID | | 回调在 JNI 线程 | 不能直接操作 UI | `TsListener` 回调 → ViewModel + StateFlow 中转 | | 所有字段为 getter/setter | Kotlin 不友好 | `TsModels` 包装为 `val`/`var` 属性 | --- ## 六、文件清单 | 文件 | 说明 | | --- | --- | | `go/teamspeak/bridge.go` | Go 侧桥接,导出 TSClient + EventCallback | | `android/app/libs/teamspeak.aar` | gomobile 编译产物 | | `android/app/src/main/java/com/tsmobile/app/TsClient.kt` | Kotlin 友好封装(TsClient 对象) | | `android/app/src/main/java/com/tsmobile/app/TsModels.kt` | Kotlin 数据类(TsChannel, TsClientInfo 等) | | `android/app/src/main/java/com/tsmobile/app/TSBridge.kt` | 应用层桥接(单例) | --- ## 七、验收标准 | # | 验证项 | 验证方法 | | --- | --- | --- | | 1 | bridge.go 可编译 | `cd go && go build ./teamspeak` 无报错 | | 2 | gomobile 生成 AAR | `gomobile bind ...` 成功 | | 3 | TsClient.connect() 可调用 | 编译通过,连接测试服务器成功 | | 4 | TsListener 回调正常 | 连接后收到 `onConnected`、`onClientEnter` 等 | | 5 | 查询返回 Kotlin 类型 | `getChannelList()` 返回 `List` | | 6 | 操作方法正常 | `sendTextMessage`、`clientMove` 等返回 true | | 7 | 断开连接无崩溃 | `disconnect()` 后应用正常退出 | --- ## 八、参考文档 - `docs/sdk-bridge-api.md` — TsClient API 完整参考 - `docs/流程/00_总览.md` — 运行架构、三条核心通道 - `docs/流程/01_连接服务器.md` — 连接时序、事件依赖 - `CLAUDE.md` — gomobile 限制说明、JNI 线程注意事项