首次推送

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
+424
View File
@@ -0,0 +1,424 @@
# 步骤 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 线程注意事项