# Bridge 层 API 文档

> 本文档是 `go/teamspeak/bridge.go` + `go/teamspeak/kotlin_api.go` 的完整 API 参考。
> 两个文件共同构成 gomobile 导出的 `teamspeak` 包，供 Kotlin 侧通过 `TSBridge` 调用。
>
> **gomobile 导出约定**：
> - 返回 `string`：空字符串 `""` = 成功，非空 = 错误信息
> - 返回 JSON `string`：查询结果以 JSON 编码，`"[]"` / `"{}"` 表示空或错误
> - 不支持 `[]string`、`[]*T`、`error` 等 Go 类型，复杂数据一律通过 JSON 传递
> - 回调通过 `EventCallback` 接口定义，所有 JNI 回调在同一线程顺序执行

---

## 一、客户端构造

### NewClient

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `NewClient` | 本地调用 | `NewClient() *TSClient` | 创建客户端实例 | 创建一个空的 TSClient 实例。不建立网络连接，不生成 Identity。需要随后调用 `Connect` 或 `ConnectWithIdentity` 发起连接。 |

---

## 二、Identity 管理

### GenerateIdentity

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GenerateIdentity` | 本地调用 | `GenerateIdentity(securityLevel int) string` | 生成加密身份 | 生成 TeamSpeak 加密身份（ECDSA P-256 密钥对），返回序列化字符串 `"base64PrivateKey:offset"`。`securityLevel` 推荐值 8（2048-bit RSA challenge），值越大生成越慢。Kotlin 侧应将返回值持久化到 SharedPreferences / DataStore，后续通过 `ConnectWithIdentity` 复用。返回空字符串表示生成失败。 |

### ConnectWithIdentity

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ConnectWithIdentity` | 客户端请求 | `(c *TSClient) ConnectWithIdentity(identityStr, host, nickname, password, defaultChannel, defaultChannelPassword string, cb EventCallback) string` | 用已有身份连接 | 使用 `GenerateIdentity` 生成并持久化的 identity 字符串连接服务器。与 `Connect` 行为相同（注册事件 → 发起 UDP 连接 → 等待握手完成，30 秒超时），但复用已有身份而非每次生成新的。`password`、`defaultChannel`、`defaultChannelPassword` 为空时忽略。返回空字符串表示连接成功。 |

---

## 三、连接管理

### Connect

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `Connect` | 客户端请求 | `(c *TSClient) Connect(host, nickname, password, defaultChannel, defaultChannelPassword string, cb EventCallback) string` | 连接服务器 | 每次生成新 Identity，注册全部事件回调，发起 UDP 连接并等待握手完成（30 秒超时）。`host` 支持域名/IP/TSDNS，可带端口号（默认 9987）。`password` 为服务器密码（可选）。`defaultChannel` 为连接后自动加入的频道名（可选）。`defaultChannelPassword` 为默认频道密码（可选）。返回空字符串表示连接成功。 |

### Disconnect

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `Disconnect` | 客户端请求 | `(c *TSClient) Disconnect()` | 断开连接 | 停止事件消费协程，发送 disconnect 命令通知服务器，清理客户端状态。断开后触发 `OnDisconnected` 回调。可安全重复调用。 |

### IsConnected

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `IsConnected` | 本地调用 | `(c *TSClient) IsConnected() bool` | 查询连接状态 | 返回当前是否已连接且客户端实例有效。线程安全。 |

### GetClientID

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetClientID` | 本地调用 | `(c *TSClient) GetClientID() int` | 获取自身客户端 ID | 返回服务器分配的当前客户端 ID（clid）。未连接时返回 0。ID 由 SDK 本地缓存，不产生网络请求。 |

### GetChannelID

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetChannelID` | 客户端请求 | `(c *TSClient) GetChannelID() string` | 获取自身所在频道 ID | 通过 `clientinfo clid=self` 查询当前客户端所在频道 ID。返回频道 ID 字符串，未连接或查询失败返回 `"0"`。 |

---

## 四、频道查询

### GetChannelsJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetChannelsJSON` | 客户端请求 | `(c *TSClient) GetChannelsJSON() string` | 获取基础频道列表 | 调用 `channellist` 协议命令，返回 JSON 数组。每个元素包含 `id`、`name`、`parentId`、`description` 四个字段。ID 和 ParentID 均为字符串格式的 uint64。未连接返回 `"[]"`。 |

**返回示例**：

```json
[{"id":"1","name":"Lobby","parentId":"0","description":""}]
```

### GetChannelsDetailedJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetChannelsDetailedJSON` | 客户端请求 | `(c *TSClient) GetChannelsDetailedJSON() string` | 获取详细频道列表 | 调用 `channellist -topic -flags -voice -limits -icon` 协议命令，返回 JSON 数组。比 `GetChannelsJSON` 多出 14 个字段：`topic`、`order`、`codec`、`codecQuality`、`neededTalkPower`、`maxClients`、`maxFamilyClients`、`isMaxClientsUnlimited`、`isMaxFamilyClientsUnlimited`、`isPermanent`、`isSemiPermanent`、`isDefault`、`isPassword`、`isOrdered`、`iconId`、`neededModifyPower`。用于频道列表页 UI 渲染（密码图标🔒、永久标记📌、人数限制👥等）。未连接返回 `"[]"`。 |

**返回示例**：

```json
[{
  "id": "1", "name": "Lobby", "parentId": "0", "topic": "欢迎",
  "order": "0", "codec": 4, "codecQuality": 7, "neededTalkPower": 0,
  "maxClients": -1, "maxFamilyClients": -1,
  "isMaxClientsUnlimited": true, "isMaxFamilyClientsUnlimited": true,
  "isPermanent": true, "isSemiPermanent": false, "isDefault": true,
  "isPassword": false, "isOrdered": false, "iconId": "0", "neededModifyPower": 75
}]
```

### ListChannelsSortedJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ListChannelsSortedJSON` | 客户端请求 | `(c *TSClient) ListChannelsSortedJSON() string` | 获取去重+排序的详细频道列表 | 底层同样调用 `channellist -topic -flags -voice -limits -icon`，但额外处理：①按 ID 去重（保留最后出现的条目）；②按 `channel_order` 排序，先顶层频道再子频道（树形扁平化：顶层频道按 order 排序，每个频道的子频道紧跟其后并按 order 排序）。返回 JSON 数组，字段与 `GetChannelsDetailedJSON` 完全一致。适合频道列表页直接展示，无需客户端侧再做排序和去重。未连接返回 `"[]"`。 |

### GetChannelDetailInfoJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetChannelDetailInfoJSON` | 客户端请求 | `(c *TSClient) GetChannelDetailInfoJSON(channelIDStr string) string` | 获取单频道详情 | 调用 `channelinfo cid=X` 协议命令，返回 JSON 对象。与 `GetChannelsDetailedJSON` 的区别：本方法返回单个频道的完整信息（含 `description` 完整描述和 `bannerGfxUrl`），适合频道详情面板展示。`channelIDStr` 为频道 ID 字符串。未连接或无效 ID 返回 `"{}"`。 |

**返回示例**：

```json
{
  "id": "1", "parentId": "0", "name": "Lobby", "topic": "欢迎",
  "description": "这是频道的完整描述...", "codec": 4, "codecQuality": 7,
  "maxClients": -1, "maxFamilyClients": -1, "neededTalkPower": 0,
  "iconId": "0", "isPermanent": true, "isSemiPermanent": false,
  "isDefault": true, "isPassword": false, "order": "0", "bannerGfxUrl": ""
}
```

### FindChannelsJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `FindChannelsJSON` | 客户端请求 | `(c *TSClient) FindChannelsJSON(pattern string) string` | 按名称搜索频道 | 调用 `channelfind pattern=X` 协议命令，返回 JSON 数组。每个元素包含 `id` 和 `name`。`pattern` 支持通配符。用于频道搜索/快速跳转功能。未连接或无匹配返回 `"[]"`。 |

**返回示例**：

```json
[{"id":"5","name":"Gaming Room"},{"id":"12","name":"Gaming Lounge"}]
```

---

## 五、客户端查询

### GetClientsJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetClientsJSON` | 客户端请求 | `(c *TSClient) GetClientsJSON() string` | 获取在线客户端列表 | 调用 `clientlist -uid -away -voice -groups` 协议命令，返回 JSON 数组。每个元素包含 `id`（clid）、`nickname`、`uid`、`channelId`、`serverGroups`（字符串数组）、`isSelf`（是否是当前用户）。自动比对 `ClientID()` 标记 `isSelf` 字段。未连接返回 `"[]"`。 |

**返回示例**：

```json
[
  {"id":1,"nickname":"Admin","uid":"abc123","channelId":"1","serverGroups":["6","9"],"isSelf":true},
  {"id":2,"nickname":"User","uid":"def456","channelId":"2","serverGroups":[],"isSelf":false}
]
```

### GetClientDetailInfoJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetClientDetailInfoJSON` | 客户端请求 | `(c *TSClient) GetClientDetailInfoJSON(clid int) string` | 获取单客户端详情 | 调用 `clientinfo clid=X` 协议命令，返回 JSON 对象。比 `GetClientsJSON` 的列表项多出 `away`、`awayMessage`、`inputMuted`、`outputMuted`、`platform`、`version`、`ip`、`created`、`lastConnected`、`totalConnections`、`description`、`iconId` 等字段。用于点击客户端后的详情弹窗。`clid` 为客户端 ID。未连接返回 `"{}"`。 |

**返回示例**：

```json
{
  "id": "1", "nickname": "Admin", "uid": "abc123", "channelId": "1",
  "type": 0, "serverGroups": ["6","9"],
  "away": false, "awayMessage": "", "inputMuted": false, "outputMuted": false,
  "platform": "Windows", "version": "3.x.x", "ip": "192.168.1.1",
  "created": "1700000000", "lastConnected": "1710000000", "totalConnections": 42,
  "description": "", "iconId": "0"
}
```

### FindClientByNameJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `FindClientByNameJSON` | 客户端请求 | `(c *TSClient) FindClientByNameJSON(nickname string) string` | 按昵称搜索数据库客户端 | 调用 `clientdbfind pattern=X -uid` 协议命令，返回 JSON 对象 `{"uid":"xxx","dbid":"123"}`。搜索服务器数据库中注册过的客户端（不要求在线）。未连接或无匹配返回 `"{}"`。 |

### FindClientByDBIDJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `FindClientByDBIDJSON` | 客户端请求 | `(c *TSClient) FindClientByDBIDJSON(dbidStr string) string` | 按 DBID 查找客户端 UID | 调用 `clientdbfind -uid cldbid=X` 协议命令，返回 JSON 对象 `{"uid":"xxx"}`。通过数据库 ID 反查唯一标识。`dbidStr` 为数据库 ID 字符串。未连接或无匹配返回 `"{}"`。 |

### ListDBClientsJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ListDBClientsJSON` | 客户端请求 | `(c *TSClient) ListDBClientsJSON(start, duration int) string` | 获取数据库客户端列表 | 调用 `clientdblist start=X duration=X` 协议命令，返回 JSON 数组。每个元素包含 `dbid`、`uid`、`nickname`、`created`、`lastConnected`、`totalConnections`、`description`。`start` 为起始位置，`duration` 为返回数量（0=全部）。用于管理功能中的用户数据库浏览。未连接返回 `"[]"`。 |

---

## 六、服务器信息

### GetServerInfoJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetServerInfoJSON` | 客户端请求 | `(c *TSClient) GetServerInfoJSON() string` | 获取服务器信息 | 调用 `serverinfo` 协议命令，返回 JSON 对象。包含 `name`（服务器名）、`welcomeMessage`、`maxClients`、`clientsOnline`、`channelsOnline`、`uptime`（运行秒数）、`version`、`platform`、`created`（创建时间戳）、`iconId`、`defaultServerGroup`、`defaultChannelGroup`。用于服务器详情展示。未连接返回 `"{}"`。 |

**返回示例**：

```json
{
  "name": "My TeamSpeak Server", "welcomeMessage": "Welcome!",
  "maxClients": 100, "clientsOnline": 5, "channelsOnline": 3,
  "uptime": "86400", "version": "3.13.7", "platform": "Linux",
  "created": "1700000000", "iconId": "0",
  "defaultServerGroup": 8, "defaultChannelGroup": 1
}
```

---

## 七、文本消息与 Poke

### SendTextMessage

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `SendTextMessage` | 客户端请求 | `(c *TSClient) SendTextMessage(targetMode int, targetIDStr string, message string) string` | 发送文字消息 | 发送文本消息到指定目标。`targetMode`: 1=私聊（target 为客户端 ID）、2=频道消息（target 为频道 ID）、3=服务器消息（target 忽略）。`targetIDStr` 为目标 ID 字符串。`message` 为消息内容。返回空字符串表示发送成功。 |

### SendChannelMessage

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `SendChannelMessage` | 客户端请求 | `(c *TSClient) SendChannelMessage(channelIDStr, message string) string` | 发送频道消息 | `SendTextMessage(targetMode=2, ...)` 的快捷方式。`channelIDStr` 为频道 ID 字符串。返回空字符串表示发送成功。 |

### Poke

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `Poke` | 客户端请求 | `(c *TSClient) Poke(clidStr string, message string) string` | 发送 Poke | 向指定客户端发送 Poke（戳一戳）消息。`clidStr` 为目标客户端 ID 字符串。`message` 为 Poke 文本。对方会收到弹窗通知。返回空字符串表示发送成功。 |

---

## 八、频道切换与客户端移动

### MoveToChannel

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `MoveToChannel` | 客户端请求 | `(c *TSClient) MoveToChannel(channelIDStr, password string) string` | 移动自己到目标频道 | 将当前客户端移动到指定频道。`channelIDStr` 为目标频道 ID 字符串。`password` 为频道密码（无密码传空字符串）。调用 `ClientMove(selfID, channelID, password)`。返回空字符串表示命令已发送，实际移动需等待 `OnClientMoved` 事件确认。 |

### MoveClient

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `MoveClient` | 客户端请求 | `(c *TSClient) MoveClient(clientID int, channelIDStr, password string) string` | 移动指定客户端到目标频道 | 将任意客户端移动到指定频道（需要权限）。`clientID` 为要移动的客户端 ID（clid）。`channelIDStr` 为目标频道 ID 字符串。`password` 为频道密码（可选）。返回空字符串表示命令已发送。 |

### MoveChannel

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `MoveChannel` | 客户端请求 | `(c *TSClient) MoveChannel(channelIDStr, parentIDStr, orderStr string) string` | 移动频道位置 | 移动频道到新的父频道下或调整排序。`channelIDStr` 为要移动的频道 ID。`parentIDStr` 为新的父频道 ID（`"0"` = 顶层）。`orderStr` 为排序位置（`"0"` = 最顶部）。返回空字符串表示成功。 |

---

## 九、频道管理

### CreateChannelJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `CreateChannelJSON` | 客户端请求 | `(c *TSClient) CreateChannelJSON(name, propertiesJSON string) string` | 创建频道 | 调用 `channelcreate channel_name=X [params...]`。`name` 为频道名。`propertiesJSON` 为 JSON 对象，包含可选属性如 `{"channel_topic":"主题","channel_flag_permanent":"1","cpid":"父频道ID","channel_password":"密码","channel_maxclients":"10","channel_codec":"4"}`。返回新频道 ID 字符串，空字符串表示失败。 |

### EditChannelJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `EditChannelJSON` | 客户端请求 | `(c *TSClient) EditChannelJSON(channelIDStr, propertiesJSON string) string` | 编辑频道属性 | 调用 `channeledit cid=X [params...]`。`channelIDStr` 为频道 ID 字符串。`propertiesJSON` 为 JSON 对象，包含要修改的属性。返回空字符串表示成功。 |

### DeleteChannel

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `DeleteChannel` | 客户端请求 | `(c *TSClient) DeleteChannel(channelIDStr string, force bool) string` | 删除频道 | 调用 `channeldelete cid=X force=X`。`channelIDStr` 为频道 ID 字符串。`force=true` 强制删除（含子频道），`false` 仅在频道为空时删除。返回空字符串表示成功。 |

---

## 十、客户端管理

### UpdateSelfJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `UpdateSelfJSON` | 客户端请求 | `(c *TSClient) UpdateSelfJSON(propertiesJSON string) string` | 更新自身属性 | 调用 `clientupdate [params...]`。`propertiesJSON` 为 JSON 对象。常用参数：`client_nickname`（新昵称）、`client_away`（`"1"`/`"0"` 离开状态）、`client_away_message`（离开消息）、`client_input_muted`（`"1"`/`"0"` 输入静音）、`client_output_muted`（`"1"`/`"0"` 输出静音）、`client_phonetic_nickname`（语音昵称）。返回空字符串表示成功。 |

**用法示例**：

```
UpdateSelfJSON('{"client_nickname":"NewName","client_away":"1","client_away_message":"AFK"}')
```

### KickClient

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `KickClient` | 客户端请求 | `(c *TSClient) KickClient(clid int, reasonID int, reasonMsg string) string` | 踢出客户端 | 调用 `clientkick clid=X reasonid=X reasonmsg=X`。`clid` 为客户端 ID。`reasonID`: 4=从频道踢出，5=从服务器踢出。`reasonMsg` 为踢出原因文本。返回空字符串表示成功。 |

---

## 十一、语音

### SendVoice

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `SendVoice` | 客户端请求 | `(c *TSClient) SendVoice(data []byte, codec int64) string` | 发送语音帧 | 发送原始 Opus 编码的语音帧到当前频道。`data` 为 Opus 编码的音频数据（20ms 帧）。`codec`: 4=Opus Voice（语音通话，默认），5=Opus Music（高保真音频）。通过 UDP 传输。返回空字符串表示发送成功。 |

---

## 十二、Ban 管理

### ListBansJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ListBansJSON` | 客户端请求 | `(c *TSClient) ListBansJSON() string` | 获取封禁列表 | 调用 `banlist` 协议命令，返回 JSON 数组。每个元素包含 `banId`、`ip`、`name`、`uid`、`created`（时间戳）、`invokerName`（操作者）、`invokerUid`、`reason`、`enforcement`（是否立即执行）。未连接返回 `"[]"`。 |

### AddBan

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `AddBan` | 客户端请求 | `(c *TSClient) AddBan(ip, name, uid string, timeSeconds int, reason string) string` | 添加封禁 | 调用 `banadd ip=X name=X uid=X time=X banreason=X`。`ip`/`name`/`uid` 至少指定一个，可组合使用。`timeSeconds` 为封禁时长（秒），0=永久。`reason` 为封禁原因（可选）。返回空字符串表示成功。 |

### DeleteBan

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `DeleteBan` | 客户端请求 | `(c *TSClient) DeleteBan(banIDStr string) string` | 删除封禁 | 调用 `bandel banid=X`。`banIDStr` 为封禁 ID 字符串（从 `ListBansJSON` 获取）。返回空字符串表示成功。 |

### DeleteAllBans

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `DeleteAllBans` | 客户端请求 | `(c *TSClient) DeleteAllBans() string` | 清除所有封禁 | 调用 `bandelall` 协议命令，删除服务器上的全部封禁记录。返回空字符串表示成功。 |

---

## 十三、Token 管理

### ListTokensJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ListTokensJSON` | 客户端请求 | `(c *TSClient) ListTokensJSON() string` | 获取权限密钥列表 | 调用 `tokenlist` 协议命令，返回 JSON 数组。每个元素包含 `token`（密钥字符串）、`tokenType`（0=服务器组, 1=频道组）、`tokenId1`（组 ID）、`tokenId2`（频道 ID，仅 tokenType=1）、`created`（创建时间戳）、`description`。未连接返回 `"[]"`。 |

### UseToken

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `UseToken` | 客户端请求 | `(c *TSClient) UseToken(token string) string` | 激活权限密钥 | 调用 `tokenuse token=X`。`token` 为权限密钥字符串。激活后自动获得对应的服务器组或频道组权限。返回空字符串表示成功。 |

---

## 十四、投诉管理

### ListComplaintsJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ListComplaintsJSON` | 客户端请求 | `(c *TSClient) ListComplaintsJSON(targetDBIDStr string) string` | 获取投诉列表 | 调用 `complainlist [tcldbid=X]` 协议命令，返回 JSON 数组。每个元素包含 `fromDbid`（投诉者 DBID）、`toDbid`（被投诉者 DBID）、`message`（投诉内容）、`timestamp`（投诉时间戳）。`targetDBIDStr` 为 `"0"` 或空字符串时返回全部投诉。未连接返回 `"[]"`。 |

### AddComplaint

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `AddComplaint` | 客户端请求 | `(c *TSClient) AddComplaint(targetDBIDStr, message string) string` | 提交投诉 | 调用 `complainadd tcldbid=X message=X`。`targetDBIDStr` 为被投诉者的数据库 ID 字符串。`message` 为投诉内容。返回空字符串表示成功。 |

### DeleteComplaint

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `DeleteComplaint` | 客户端请求 | `(c *TSClient) DeleteComplaint(targetDBIDStr, fromDBIDStr string) string` | 删除投诉 | 调用 `complaindel tcldbid=X fcldbid=X`。`targetDBIDStr` 为被投诉者 DBID，`fromDBIDStr` 为投诉者 DBID。返回空字符串表示成功。 |

---

## 十五、文件传输

### ListFilesJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `ListFilesJSON` | 客户端请求 | `(c *TSClient) ListFilesJSON(channelIDStr, path string) string` | 获取频道文件列表 | 调用 `ftgetfilelist cid=X path=X` 协议命令，返回 JSON 数组。每个元素包含 `name`（文件/目录名）、`size`（字节数，目录为 0）、`dateTime`（修改时间戳）、`isFile`（true=文件, false=目录）。`channelIDStr` 为频道 ID 字符串。`path` 为虚拟路径，根目录为 `"/"`。未连接返回 `"[]"`。 |

### FileTransferInitUploadJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `FileTransferInitUploadJSON` | 客户端请求 | `(c *TSClient) FileTransferInitUploadJSON(channelIDStr, path string, size int64, overwrite bool) string` | 初始化文件上传 | 调用 `ftinitupload cid=X path=X size=X overwrite=X`，等待服务器返回传输参数。返回 JSON 对象：`port`（TCP 端口）、`key`（传输密钥）、`clientFileTransferID`、`serverFileTransferID`、`seekPosition`（断点续传位置）。`channelIDStr` 为频道 ID。`path` 为目标路径。`size` 为文件字节数。`overwrite` 为是否覆盖。TCP 连接 host 为当前服务器地址。未连接返回 `"{}"`。 |

### FileTransferInitDownloadJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `FileTransferInitDownloadJSON` | 客户端请求 | `(c *TSClient) FileTransferInitDownloadJSON(channelIDStr, path string) string` | 初始化文件下载 | 调用 `ftinitdownload cid=X path=X`，等待服务器返回传输参数。返回 JSON 对象：`port`（TCP 端口）、`key`（传输密钥）、`size`（文件字节数）、`clientFileTransferID`、`serverFileTransferID`。`channelIDStr` 为频道 ID。`path` 为源路径。未连接返回 `"{}"`。 |

### DeleteFile

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `DeleteFile` | 客户端请求 | `(c *TSClient) DeleteFile(channelIDStr, pathsJSON string) string` | 删除频道文件 | 调用 `ftdeletefile cid=X path=X [path2=X ...]`。`channelIDStr` 为频道 ID 字符串。`pathsJSON` 为要删除的文件路径 JSON 数组，如 `["/file1.txt","/dir/file2.txt"]`。返回空字符串表示成功。 |

---

## 十六、批量查询

### GetInitialSyncJSON

| 名字 | 触发形式 | 用法 | 作用 | 详细描述 |
|------|----------|------|------|----------|
| `GetInitialSyncJSON` | 客户端请求 | `(c *TSClient) GetInitialSyncJSON() string` | 首次同步批量查询 | 一次性返回连接后首次同步所需的全部数据，将 4 次 JNI 调用合并为 1 次。内部依次调用 `GetChannelsDetailedJSON`、`GetClientsJSON`、`GetClientID`、`GetChannelID`、`GetServerInfoJSON`，合并为单个 JSON 对象返回。任一子查询失败时对应字段为 null/空，不影响其他字段。未连接返回 `"{}"`。 |

**返回格式**：

```json
{
  "channels": [
    {"id":"1","name":"Lobby","parentId":"0","topic":"","order":"0","codec":4,...}
  ],
  "clients": [
    {"id":1,"nickname":"Admin","uid":"abc","channelId":"1","serverGroups":[],"isSelf":true}
  ],
  "selfId": 1,
  "selfChannelId": "1",
  "server": {
    "name":"My Server","welcomeMessage":"","maxClients":100,"clientsOnline":5,...
  }
}
```

---

## 十七、事件回调接口

### EventCallback

所有事件通过 `EventCallback` 接口回调。Kotlin 侧实现此接口并传入 `Connect` 或 `ConnectWithIdentity`。所有回调在同一个 goroutine 中顺序执行（通过内部事件队列保证），无需担心并发问题。

| 回调方法 | 触发时机 | 参数 | 描述 |
|----------|----------|------|------|
| `OnConnected()` | 连接握手完成 | 无 | 服务器握手成功，可以开始发送业务命令。此时应执行首次同步（调用 `GetInitialSyncJSON`）。 |
| `OnDisconnected(message string)` | 连接断开 | `message`: 错误原因 | 网络中断或服务器主动断开。`message` 为空表示正常断开。此时应清理 UI 状态并提示用户。 |
| `OnTextMessage(msg *TextMsg)` | 收到文本消息 | `msg.TargetMode`: 1=私聊/2=频道/3=服务器；`msg.TargetID`: 目标 ID；`msg.InvokerName`: 发送者昵称；`msg.InvokerUID`: 发送者 UID；`msg.Message`: 消息内容 | 收到其他客户端发送的文本消息。按 TargetMode + Target 归档到正确的会话。 |
| `OnClientEnter(client *Client)` | 用户进入视野 | `client.ID`: 客户端 ID；`client.Nickname`: 昵称；`client.UID`: UID；`client.ChannelID`: 所在频道 ID | 新客户端进入服务器。应添加到本地成员表。`client.ChannelID` 来自 `notifycliententerview`，可能不准确，需补偿同步。 |
| `OnClientLeave(id int, reasonMsg string)` | 用户离开 | `id`: 客户端 ID；`reasonMsg`: 离开原因 | 客户端离开服务器。应从本地成员表移除。 |
| `OnClientMoved(id int, targetChannelID string)` | 用户移动频道 | `id`: 客户端 ID；`targetChannelID`: 目标频道 ID | 客户端在频道间移动。当 `id == GetClientID()` 时，表示自己被移动（含密码验证结果）。应更新成员所在频道。 |
| `OnKicked(reason string)` | 被踢出 | `reason`: 踢出原因 | 自己被从频道或服务器踢出。应停止语音采集，清理会话状态，显示被踢原因并允许重连。 |
| `OnVoiceData(clientID int, data []byte, codec int)` | 收到语音 | `clientID`: 发送者 ID；`data`: Opus 编码帧；`codec`: 4=Opus Voice/5=Opus Music | 收到同频道其他客户端的语音数据。`data` 可直接送入 Opus 解码器。回调中不要做耗时操作，应推入 channel 由独立协程解码播放。 |
| `OnPoked(event *PokeEvent)` | 被 Poke | `event.InvokerID`: 发送者 ID；`event.InvokerName`: 发送者昵称；`event.InvokerUID`: 发送者 UID；`event.Message`: Poke 消息 | 收到其他用户的 Poke 消息。应显示 Toast 或弹窗通知。 |

### Kotlin 侧事件处理流程

以下三个事件在 Android 端有完整的 UI 处理链路：

#### 被管理员移动到指定频道（OnClientMoved）

```
Go SDK notifyclientmoved
  → EventCallback.OnClientMoved(id, targetChannelID)
  → TSBridge 转发
  → ChannelViewModel.handleClientMoved()
      → 判断 id == selfId → 标记为"被外部移动"
      → 更新 _currentChannelId
      → refreshClientList() 刷新当前频道成员
  → UI 自动更新频道列表和成员列表
```

**日志标识**: `Client moved: {id} -> channel {channelID}`、`Self moved by external action to channel {channelID}`

#### 收到频道内文本消息（OnTextMessage, targetMode=2）

```
Go SDK notifytextmessage
  → EventCallback.OnTextMessage(msg)
  → TSBridge 转发
  → ServerViewModel.handleTextMessage()
      → 解析 senderId（通过 clientlist 匹配 invokerUID）
      → 转发给 ChatViewModel
  → ChatViewModel.handleTextMessage()
      → 判断 targetMode=2（频道消息）
      → 归档到对应会话（频道 ID 为 key）
      → 如果匹配当前打开的聊天，更新实时消息列表
```

**日志标识**: `onTextMessage: targetMode=2, targetID={channelID}, invokerName={name}, message={content}`

#### 被 Poke 带文本消息（OnPoked）

```
Go SDK notifyclientpoke
  → EventCallback.OnPoked(event)
  → TSBridge 转发
  → ServerViewModel.handlePoked()
      → 更新 _pokeNotification 状态
      → triggerVibration() 震动反馈（需 VIBRATE 权限）
      → 5 秒后自动隐藏通知
  → UI 显示 Poke 弹窗（发送者昵称 + 消息内容）
```

**日志标识**: `Poked by {name}: {message}`

**注意**: Poke 震动需要 `android.permission.VIBRATE` 权限（AndroidManifest.xml 中声明即可，无需运行时请求）。

---

## 十八、导出数据类型

以下类型通过 gomobile 导出到 Kotlin 侧，可直接在回调参数中使用。

### Channel

| 字段 | 类型 | 描述 |
|------|------|------|
| `ID` | `string` | 频道 ID |
| `Name` | `string` | 频道名称 |
| `ParentID` | `string` | 父频道 ID |
| `Description` | `string` | 频道描述 |

### Client

| 字段 | 类型 | 描述 |
|------|------|------|
| `ID` | `int` | 客户端 ID（clid） |
| `Nickname` | `string` | 昵称 |
| `UID` | `string` | 唯一标识 |
| `ChannelID` | `string` | 所在频道 ID |
| `IsSelf` | `bool` | 是否是当前用户（仅 `GetClientsJSON` 中自动标记） |

### TextMsg

| 字段 | 类型 | 描述 |
|------|------|------|
| `InvokerName` | `string` | 发送者昵称 |
| `InvokerUID` | `string` | 发送者唯一标识 |
| `Message` | `string` | 消息内容 |
| `TargetMode` | `int` | 1=私聊, 2=频道, 3=服务器 |
| `TargetID` | `string` | 目标 ID |

### PokeEvent

| 字段 | 类型 | 描述 |
|------|------|------|
| `InvokerID` | `int` | 发送者客户端 ID |
| `InvokerName` | `string` | 发送者昵称 |
| `InvokerUID` | `string` | 发送者唯一标识 |
| `Message` | `string` | Poke 消息内容 |

---

## 十九、JSON 数据结构参考

以下 JSON 结构由查询方法返回，供 Kotlin 侧反序列化使用。

### channelJSON（GetChannelsJSON / FindChannelsJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | `string` | 频道 ID |
| `name` | `string` | 频道名称 |
| `parentId` | `string` | 父频道 ID（GetChannelsJSON 有，FindChannelsJSON 无） |
| `description` | `string` | 频道描述（GetChannelsJSON 有，FindChannelsJSON 无） |

### channelDetailedJSON（GetChannelsDetailedJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | `string` | 频道 ID |
| `name` | `string` | 频道名称 |
| `parentId` | `string` | 父频道 ID |
| `topic` | `string` | 频道主题 |
| `order` | `string` | 排序顺序 |
| `codec` | `int` | 编解码器（4=Opus Voice, 5=Opus Music） |
| `codecQuality` | `int` | 编解码质量（0-10） |
| `neededTalkPower` | `int` | 发言所需权限等级 |
| `maxClients` | `int` | 最大客户端数（-1=无限） |
| `maxFamilyClients` | `int` | 最大族客户端数（-1=无限） |
| `isMaxClientsUnlimited` | `bool` | 是否无限人数 |
| `isMaxFamilyClientsUnlimited` | `bool` | 是否无限族人数 |
| `isPermanent` | `bool` | 永久频道 |
| `isSemiPermanent` | `bool` | 半永久频道 |
| `isDefault` | `bool` | 默认频道 |
| `isPassword` | `bool` | 有密码 |
| `isOrdered` | `bool` | 手动排序 |
| `iconId` | `string` | 频道图标 ID |
| `neededModifyPower` | `int` | 修改频道所需权限 |

### channelDetailedInfoJSON（GetChannelDetailInfoJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | `string` | 频道 ID |
| `parentId` | `string` | 父频道 ID |
| `name` | `string` | 频道名称 |
| `topic` | `string` | 频道主题 |
| `description` | `string` | 完整描述（可能很长） |
| `codec` | `int` | 编解码器 |
| `codecQuality` | `int` | 编解码质量 |
| `maxClients` | `int` | 最大人数 |
| `maxFamilyClients` | `int` | 最大族人数 |
| `neededTalkPower` | `int` | 发言权限 |
| `iconId` | `string` | 图标 ID |
| `isPermanent` | `bool` | 永久 |
| `isSemiPermanent` | `bool` | 半永久 |
| `isDefault` | `bool` | 默认 |
| `isPassword` | `bool` | 有密码 |
| `order` | `string` | 排序 |
| `bannerGfxUrl` | `string` | Banner 图片 URL |

### clientJSON（GetClientsJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | `int` | 客户端 ID |
| `nickname` | `string` | 昵称 |
| `uid` | `string` | 唯一标识 |
| `channelId` | `string` | 所在频道 ID |
| `serverGroups` | `[]string` | 所在服务器组 ID 列表 |
| `isSelf` | `bool` | 是否是当前用户 |

### clientDetailedInfoJSON（GetClientDetailInfoJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `id` | `string` | 客户端 ID |
| `nickname` | `string` | 昵称 |
| `uid` | `string` | 唯一标识 |
| `channelId` | `string` | 所在频道 ID |
| `type` | `int` | 客户端类型 |
| `serverGroups` | `[]string` | 服务器组 |
| `away` | `bool` | 是否离开 |
| `awayMessage` | `string` | 离开消息 |
| `inputMuted` | `bool` | 输入静音 |
| `outputMuted` | `bool` | 输出静音 |
| `platform` | `string` | 平台 |
| `version` | `string` | 版本 |
| `ip` | `string` | IP 地址 |
| `created` | `string` | 首次连接时间戳 |
| `lastConnected` | `string` | 最近连接时间戳 |
| `totalConnections` | `int` | 总连接次数 |
| `description` | `string` | 用户描述 |
| `iconId` | `string` | 图标 ID |

### serverInfoJSON（GetServerInfoJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `name` | `string` | 服务器名称 |
| `welcomeMessage` | `string` | 欢迎消息 |
| `maxClients` | `int` | 最大客户端数 |
| `clientsOnline` | `int` | 在线客户端数 |
| `channelsOnline` | `int` | 在线频道数 |
| `uptime` | `string` | 运行时长（秒） |
| `version` | `string` | 服务器版本 |
| `platform` | `string` | 服务器平台 |
| `created` | `string` | 创建时间戳 |
| `iconId` | `string` | 服务器图标 ID |
| `defaultServerGroup` | `int` | 默认服务器组 ID |
| `defaultChannelGroup` | `int` | 默认频道组 ID |

### dbClientJSON（ListDBClientsJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `dbid` | `string` | 数据库 ID |
| `uid` | `string` | 唯一标识 |
| `nickname` | `string` | 昵称 |
| `created` | `string` | 首次连接时间戳 |
| `lastConnected` | `string` | 最近连接时间戳 |
| `totalConnections` | `int` | 总连接次数 |
| `description` | `string` | 描述 |

### banEntryJSON（ListBansJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `banId` | `string` | 封禁 ID |
| `ip` | `string` | IP |
| `name` | `string` | 名称 |
| `uid` | `string` | 唯一标识 |
| `created` | `string` | 封禁时间戳 |
| `invokerName` | `string` | 操作者昵称 |
| `invokerUid` | `string` | 操作者 UID |
| `reason` | `string` | 封禁原因 |
| `enforcement` | `bool` | 是否立即执行 |

### fileEntryJSON（ListFilesJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `name` | `string` | 文件/目录名 |
| `size` | `string` | 字节数（目录为 "0"） |
| `dateTime` | `string` | 修改时间戳 |
| `isFile` | `bool` | true=文件, false=目录 |

### tokenEntryJSON（ListTokensJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `token` | `string` | 权限密钥字符串 |
| `tokenType` | `int` | 0=服务器组, 1=频道组 |
| `tokenId1` | `string` | 组 ID |
| `tokenId2` | `string` | 频道 ID（仅 tokenType=1） |
| `created` | `string` | 创建时间戳 |
| `description` | `string` | 描述 |

### complaintEntryJSON（ListComplaintsJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `fromDbid` | `string` | 投诉者 DBID |
| `toDbid` | `string` | 被投诉者 DBID |
| `message` | `string` | 投诉内容 |
| `timestamp` | `string` | 投诉时间戳 |

### fileTransferInitJSON（FileTransferInitUploadJSON / FileTransferInitDownloadJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `port` | `int` | TCP 传输端口 |
| `key` | `string` | 传输密钥 |
| `size` | `int` | 文件字节数（仅下载） |
| `clientFileTransferID` | `int` | 客户端传输 ID |
| `serverFileTransferID` | `int` | 服务器传输 ID |
| `seekPosition` | `int` | 断点续传位置（仅上传） |

### initialSyncJSON（GetInitialSyncJSON）

| 字段 | 类型 | 描述 |
|------|------|------|
| `channels` | `[]channelDetailedJSON` | 详细频道列表 |
| `clients` | `[]clientJSON` | 在线客户端列表 |
| `selfId` | `int` | 自身客户端 ID |
| `selfChannelId` | `string` | 自身所在频道 ID |
| `server` | `serverInfoJSON` | 服务器信息 |

---

## 二十、方法来源标记

标记每个方法的实现文件，便于定位代码。

| 方法 | 来源文件 | 说明 |
|------|----------|------|
| `NewClient` | bridge.go | |
| `GenerateIdentity` | kotlin_api.go | |
| `Connect` | bridge.go | 每次生成新 Identity |
| `ConnectWithIdentity` | kotlin_api.go | 复用已有 Identity |
| `Disconnect` | bridge.go | |
| `IsConnected` | bridge.go | |
| `GetClientID` | bridge.go | |
| `GetChannelID` | bridge.go | |
| `GetChannelsJSON` | bridge.go | |
| `GetChannelsDetailedJSON` | bridge.go | |
| `ListChannelsSortedJSON` | kotlin_api.go | 去重+排序 |
| `GetClientsJSON` | bridge.go | |
| `GetServerInfoJSON` | bridge.go | |
| `GetChannelDetailInfoJSON` | bridge.go | |
| `GetClientDetailInfoJSON` | bridge.go | |
| `ListDBClientsJSON` | bridge.go | |
| `FindChannelsJSON` | kotlin_api.go | |
| `FindClientByNameJSON` | kotlin_api.go | |
| `FindClientByDBIDJSON` | kotlin_api.go | |
| `SendTextMessage` | bridge.go | |
| `SendChannelMessage` | bridge.go | 快捷方式 |
| `Poke` | bridge.go | |
| `MoveToChannel` | bridge.go | 移动自己 |
| `MoveClient` | kotlin_api.go | 移动指定客户端 |
| `MoveChannel` | kotlin_api.go | 移动频道位置 |
| `CreateChannelJSON` | bridge.go | |
| `EditChannelJSON` | bridge.go | |
| `DeleteChannel` | bridge.go | |
| `UpdateSelfJSON` | bridge.go | |
| `KickClient` | bridge.go | |
| `SendVoice` | bridge.go | |
| `ListBansJSON` | bridge.go | |
| `AddBan` | bridge.go | |
| `DeleteBan` | bridge.go | |
| `DeleteAllBans` | kotlin_api.go | |
| `ListTokensJSON` | bridge.go | |
| `UseToken` | bridge.go | |
| `ListComplaintsJSON` | bridge.go | |
| `AddComplaint` | bridge.go | |
| `DeleteComplaint` | kotlin_api.go | |
| `ListFilesJSON` | bridge.go | |
| `FileTransferInitUploadJSON` | kotlin_api.go | |
| `FileTransferInitDownloadJSON` | kotlin_api.go | |
| `DeleteFile` | kotlin_api.go | |
| `GetInitialSyncJSON` | kotlin_api.go | |
