Files
ts-mobile-go/CLAUDE.md
T
sansenandClaude Fable 5 8716b391f6 工程改进:跨语言契约固化、日志脱敏、状态模型收敛
依据 docs/项目工程改进方案.md 实施(第五梯队 CI/协作除外)。

安全性
- Release 剥离 Log.v/d/i/w(-assumenosideeffects),保留 Log.e
- 全量频道/成员 dump、消息正文、发送者 UID 改为 BuildConfig.DEBUG 保护
  (实测 release dex 中 "Channel: id=" / "Client: id=" / selfId= 等均为 0 命中)

构建产物体积
- .gitignore 补 *.a(保留 .opus/lib 四个 ABI 预编译库)、.opus/install、
  app/libs/*.aar、dnn/torch
- git rm --cached 除名 AAR、.gradle、.opus/{build,install}、dnn/torch
  跟踪体积 50MB → 17.4MB,工作区文件不受影响

跨语言契约(本方案核心)
- 新增 go/teamspeak/contract.go:字段名常量 + BridgeContractVersion 单一事实来源
- 新增契约测试:反射断言常量与 struct tag 一致;Go/Kotlin 共用 golden 样本
- 新增 BridgeContract.kt:启动校验 AAR 契约版本,不匹配则阻止连接
- 修正文档:channel_order 是前驱频道 ID(链表指针)而非排序权重,
  按它数值排序会打乱频道树;ChannelInfo.order 在 Go 侧不存在

状态模型
- ConnectionState 增 Idle 取代 null 编码,connectionState 不再可空
- applyClients 改为按频道差分更新,避免全量刷新导致频道树整体重组
- 频道数据 freshness 统一由 Repository 维护,修复 ViewModel 与 Repository
  两份时间戳不同步导致的重复刷新

UI
- 修复 collectAsState() 在参数位置调用导致的 isSelf 快照失效
- 消除 9 处 !! 断言(ChannelListScreen)
- 硬编码 24.5/14.5dp 与 depth*24 收敛到 UiTokens.Spacing,缩进加 4 层上限

测试
- Kotlin 测试 7 → 106;Go 契约测试新增 24 个用例
- 各覆盖 InputValidator、消息送达确认、僵尸会话过滤、频道差分、
  频道顺序语义、错误分类

其他修复
- classifyError 提取为纯函数并补测试;修复 too many clones (id=521)
  未识别导致英文原文直接暴露给用户

版本号提升至 1.0.13(versionCode 13)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 14:58:15 +08:00

189 lines
9.0 KiB
Markdown
Raw 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.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
TeamSpeak Android native client with a two-layer architecture:
- **Protocol layer**: Go + [teamspeak-go](https://github.com/honeybbq/teamspeak-go) → compiled to `.aar` via gomobile
- **UI layer**: Kotlin + Jetpack Compose + Material Design 3
## Build Commands
### Full Build (Recommended)
```bash
# Windows
build.bat
# Linux/macOS
./build.sh
```
### Manual Build (Two Steps Required)
**Step 1: Compile Go → AAR**
```bash
cd go
set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 # Windows only
gomobile bind -target=android -androidapi=26 -ldflags="-linkmode=external -extldflags=-Wl,--hash-style=both" -o ../android/app/libs/teamspeak.aar ./teamspeak
```
**Step 2: Build Android APK**
```bash
cd android
gradlew.bat assembleDebug # Windows
./gradlew assembleDebug # Linux/macOS
```
### Development Workflow
- **Modified Go code** (`go/`): Re-run Step 1, then run from IDE
- **Modified Kotlin code** (`android/`): Just run from IDE (IntelliJ/Android Studio)
Open the `android/` directory (not root) in IntelliJ IDEA or Android Studio.
## Architecture
### Go ↔ Kotlin Bridge
Communication flows through two bridge layers:
1. **Go side** (`go/teamspeak/bridge.go`): Exports `TSClient` class via gomobile
2. **Kotlin side** (`android/app/src/main/java/com/tsmobile/app/TSBridge.kt`): Wraps gomobile API
**Key constraints** (gomobile limitations):
- Cannot export `[]string`, `[]*T`, or Go `error` types
- Complex data passed as JSON strings (channels, clients)
- Events delivered via callback interfaces
- All JNI callbacks serialized through an event queue to avoid threading issues
**Event flow**: Go library → event queue → single consumer goroutine → JNI callback → Kotlin callback → ViewModel
### Android MVVM + Repository Pattern
```
Go Library → TSBridge (JNI) → ServerViewModel (callbacks) → Repository (state) → ViewModels → Compose Screens
User Actions → Compose UI → ViewModel methods → TSBridge → Go Library
```
**Repository** (`data/Repository.kt`): Singleton object, single source of truth for shared state — channels, clients, channel→clients mapping, current channel, unread counts. Uses `ConcurrentHashMap` for thread-safe message archives (max 500 per session, keyed by `targetMode_targetId`).
**ViewModels**: Cross-references set by `NavGraph` (e.g. `serverViewModel.channelViewModel = channelViewModel`). `ServerViewModel` dispatches all bridge callbacks to other ViewModels.
**State machines** (sealed classes in `data/Models.kt`): `ConnectionState`, `MessageSendState`, `MessageDeliveryState`, `VoiceState`, `SyncState`, `ChannelSwitchState` — use `when` for pattern matching.
### Voice System Ownership Split
- **Go side** owns the full decode/mix pipeline: per-client jitter buffers, Opus decoders, stereo mixing. Delivers mixed PCM16 as 20ms frames (48kHz, interleaved stereo) via `onPCM` callback. Speaking detection: 400ms silence timeout.
- **Kotlin side** owns capture: `AudioRecord` (mono, 48kHz) → `OpusEncoder` → `TSBridge.sendVoice()`. Noise suppressor support. Speaking detection via RMS threshold (0.015).
### Message Delivery Confirmation
Messages appear immediately as `PENDING` in UI. Server echo (`OnTextMessage` with matching content) confirms → `SENT`. 10-second timeout → `FAILED`. Duplicate detection prevents self-message re-archival.
### Navigation Routes
Four routes in `NavGraph.kt`: `server_config` (connection form) → `channel_list` (channel tree + members) → `chat` (text chat) → `kicked` (kick notification). Navigation driven by `ConnectionState` changes via `LaunchedEffect`.
### Known SDK Limitations
- **No channel CRUD events**: The SDK does not fire events for channel create/update/delete. Channel list is refreshed after 5 minutes of staleness or before channel switch operations. Staleness is tracked by `Repository.channelsFetchedAt` and surfaced in the UI via `StaleChannelBanner`.
- **Unreliable ChannelID in enter events**: `notifycliententerview` ChannelID is not effective per SDK docs. Client enter/leave events trigger a full `clientlist` refresh instead of incremental updates. To keep this from rebuilding the whole channel tree, `Repository.applyClients` does a **per-channel differential update** (`diffChannelClients`) that reuses unchanged list instances.
- **Auto-reconnect disabled by default**: Frequent reconnect attempts cause server-side rate limiting/bans.
### Thread Safety
- `TSClient.mu` mutex protects Go-side client access
- `TSBridge.connectLock` synchronizes Kotlin-side connection lifecycle
- `connectionGeneration` counter prevents stale callbacks from reaching current session
- Event queue PCM events capped at 12 with oldest eviction to prevent buildup
## Critical Build Details
### Required Flags for gomobile
The `-ldflags` are **mandatory** to avoid runtime crashes:
1. **`-linkmode=external`**: Use NDK's external linker instead of Go's built-in linker
- **Why**: Prevents `SIGSEGV` crashes due to signal handling conflicts between Go runtime and Android
- Go uses SIGSEGV for GC and goroutine scheduling; Android's memory protection blocks this
2. **`-extldflags=-Wl,--hash-style=both`**: Generate compatible ELF hash tables
- **Why**: Go 1.24+ uses `DT_SUNW_HASH` by default; Android requires `DT_HASH` or `DT_GNU_HASH`
- Without this: `dlopen failed: empty/missing DT_HASH/DT_GNU_HASH` error
### Local Patches
The `go/_patches/github.com/honeybbq/teamspeak-go/` directory contains modified upstream code:
- Fixes 32-bit integer overflow issues (`math.MaxUint32` → platform-specific limits)
- Applied via `go.mod` replace directive: `replace github.com/honeybbq/teamspeak-go => ./_patches/github.com/honeybbq/teamspeak-go`
Do not remove this directory or the replace directive.
### Go Module and CGo
Go module name is `tsmobile`. Unlike upstream teamspeak-go (zero CGO), this project enables CGO for the Android libopus decoder. Pre-built static libraries live in `go/teamspeak/.opus/lib/{abi}/libopus.a` for all four Android ABIs.
## Common Issues
### "javac: 非法字符" or "GBK unmappable character"
Set encoding before running gomobile on Windows:
```bash
set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
```
### App crashes with "SIGSEGV" on startup
Missing `-linkmode=external` flag. Rebuild AAR with correct flags.
### App crashes with "empty/missing DT_HASH"
Missing `-Wl,--hash-style=both` flag. Rebuild AAR with correct flags.
### "Unresolved reference: teamspeak"
AAR not compiled or not in `android/app/libs/teamspeak.aar`. Run Step 1 of manual build.
## Error Conventions
- **Go bridge**: Empty string return = success, non-empty = error message. All IDs are strings for JSON transport.
- **Kotlin**: Chinese-language user-facing errors. `ServerViewModel.classifyError()` maps raw errors to friendly messages. `ChannelViewModel.mapMoveError()` handles channel switch errors.
## Testing
Run from IDE with connected device or emulator. Check Logcat in IDE for runtime logs.
```bash
# Go:音频接收管线 + 跨语言契约
cd go && go test ./teamspeak/
# Kotlin:契约反序列化 + 纯逻辑(无需设备)
cd android && ./gradlew testDebugUnitTest
```
### Cross-language contract
Go ↔ Kotlin 的 JSON 字段约定是**编译器无法校验**的边界(改字段名只会静默拿到默认值)。
三道机制钉住它,改动 bridge 字段时必须一起维护:
| 文件 | 作用 |
| --- | --- |
| `go/teamspeak/contract.go` | 字段名常量的单一事实来源 + `BridgeContractVersion` |
| `go/teamspeak/contract_test.go` / `contract_golden_test.go` | 反射断言常量与 struct tag 一致;golden 样本一致性 |
| `android/.../data/BridgeContract.kt` | 启动时校验 AAR 契约版本,不匹配则阻止连接 |
| `android/app/src/test/resources/contract/` | **Go 与 Kotlin 共用**的 golden 样本 |
| `docs/bridge-contract.md` | 逐字段对照表与 targetMode 收发能力矩阵 |
不兼容变更(字段改名/类型变更/删除)必须递增 `BridgeContractVersion` 并同步
`BridgeContract.EXPECTED_VERSION` 与该文档。
### Known gotchas fixed here
- **`ChannelInfo.order` 在 Go 侧不存在**(`channelJSON` 无 `order` 字段),
恒为默认 `0`,`buildChannelTree` 的 `sortedBy { it.order }` 是 no-op。
频道显示顺序由 TS3 `channellist` 的响应顺序决定(已真机验证与显示一致),这是正确的。
- **`channel_order` 不是排序权重,是前驱频道的 ID**(链表指针,`0` = 本层最前)。
见 `docs/teamspeak-sdk-3.5.2/doc/client/channel-sort.html`。
**不要按它数值排序**——那会打乱频道树。若要在客户端重建顺序,需按前驱指针串链。
- **私聊(targetMode=1)只能收、不能发**。`TSBridge.sendTextMessage` 仅实现 `mode=2`。
- **频道陈旧判定统一走 `Repository.isChannelDataStale()`**。
不要在 ViewModel 里另建时间戳——历史上两处时间戳不同步会导致重复刷新请求。