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

425 lines
15 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.
# 步骤 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<TsChannel>`
- 事件回调参数是 `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<TsChannel>
fun getClientList(): List<TsClientInfo>
fun getClientId(): Long
fun getClientInfo(clientId: Long): TsClientInfo?
fun getChannelInfo(channelId: Long): TsChannel?
fun getSelf(): TsSelf
fun getChannelIdsByUid(uid: String, maxDepth: Int): List<Long>
// 属性
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<String, String>, permissions: List<TSPermission>): Long
fun channelUpdate(channelId: Long, properties: Map<String, String>): 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<TsChannel>
fun getClientList(): List<TsClientInfo>
fun getClientId(): Long
fun getSelf(): TsSelf
fun getChannelInfo(channelId: Long): TsChannel?
fun getClientInfo(clientId: Long): TsClientInfo?
fun getChannelIdsByUid(uid: String): List<Long>
// === 操作 ===
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<String, String> = emptyMap(),
permissions: List<TSPermission> = emptyList()): Long
fun channelUpdate(channelId: Long, properties: Map<String, String>): 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<TsChannel>
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<TsChannel>` |
| 6 | 操作方法正常 | `sendTextMessage``clientMove` 等返回 true |
| 7 | 断开连接无崩溃 | `disconnect()` 后应用正常退出 |
---
## 八、参考文档
- `docs/sdk-bridge-api.md` — TsClient API 完整参考
- `docs/流程/00_总览.md` — 运行架构、三条核心通道
- `docs/流程/01_连接服务器.md` — 连接时序、事件依赖
- `CLAUDE.md` — gomobile 限制说明、JNI 线程注意事项