Files
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

154 lines
7.0 KiB
Go
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.
package teamspeak
// bridge-contract.md —— Go ↔ Kotlin 跨语言契约的单一事实来源。
//
// 为什么需要这个文件:
// gomobile 不能导出 []string / []*T / Go error,所以复杂数据全部走 JSON 字符串。
// 代价是**编译器无法校验字段名**:Go 侧把 `channelId` 写成 `channelID`,
// Kotlin 侧不会编译失败,只会在运行时静默拿到默认值("" / 0 / false),
// 而 Kotlin 的 `Json { ignoreUnknownKeys = true }` 让这个失效模式更隐蔽。
//
// 边界上的三种数据类型(按保护强度递减,文档必须写清属于哪一类):
//
// A 类|gomobile 导出 struct(TextMsg / PokeEvent / Channel / Client)
// 字段是编译期检查的,Go 侧改名 → Kotlin 侧编译失败。**不需要**常量保护。
//
// B 类|JSON 字符串(channelJSON / clientJSON / serverInfoJSON / ...)
// 完全无编译期保护,就是本文件存在的理由。字段名集中声明在下方常量里,
// contract_test.go 断言常量与 struct tag 永远一致——改 tag 会让测试变红。
//
// C 类|广告过但无消费方的契约(channelDetailedJSON / clientDetailedInfoJSON /
// initialSyncJSON)
// 已被 GetInitialSyncJSON 等导出,但 Kotlin 侧当前不解析。
// 声明字段名是为了钉住形状,避免「文档说有、实际对不上」。
// 若将来要消费,先跑 contract_test.go 确认字段名。
//
// 字段名的运行时事实仍是 bridge.go / kotlin_api.go 中的 `json:"..."` tag
// (encoding/json 只认 tag);本文件是声明,测试是保证两者不漂移的机制。
// BridgeContractVersion 跨语言契约版本。
//
// 递增规则:任何**不兼容**变更(字段改名、类型变更、字段删除)都必须 +1。
// 只新增 Optional 字段(Kotlin 侧有默认值)不需要 +1。
// 递增后必须同步更新:
// - android/.../TSBridge.kt 的 EXPECTED_CONTRACT_VERSION
// - docs/bridge-contract.md
const BridgeContractVersion = "1"
// ─── B 类:channelJSON ⇄ ChannelInfo ──────────────────────────
// 生产方 GetChannelsJSON();消费方 Repository.fetchChannels()。
const (
FieldChannelID = "id"
FieldChannelName = "name"
FieldChannelParentID = "parentId"
FieldChannelDescription = "description"
FieldChannelIsPassword = "isPassword"
)
// ─── B 类:clientJSON ⇄ ClientInfo ────────────────────────────
// 生产方 GetClientsJSON();消费方 Repository.fetchClients()。
const (
FieldClientID = "id"
FieldClientNickname = "nickname"
FieldClientUID = "uid"
FieldClientChannelID = "channelId"
FieldClientServerGroups = "serverGroups"
FieldClientIsSelf = "isSelf"
)
// ─── B 类:serverInfoJSON ⇄ ServerInfo ────────────────────────
// 生产方 GetServerInfoJSON();消费方 Repository / ServerViewModel。
const (
FieldServerName = "name"
FieldServerWelcomeMessage = "welcomeMessage"
FieldServerMaxClients = "maxClients"
FieldServerClientsOnline = "clientsOnline"
FieldServerChannelsOnline = "channelsOnline"
FieldServerUptime = "uptime"
FieldServerVersion = "version"
FieldServerPlatform = "platform"
FieldServerCreated = "created"
FieldServerIconID = "iconId"
FieldServerDefaultServerGroup = "defaultServerGroup"
FieldServerDefaultChannelGroup = "defaultChannelGroup"
)
// ─── B 类:DownloadFileBytesJSON ⇄ TSBridge.downloadFileBytes ─
// 生产方 DownloadFileBytesJSON();消费方 FileDownloadManager。
// 失败时返回 "{}"(而非空串),消费方以 data 为空判定失败。
const (
FieldFileData = "data"
FieldFileSize = "size"
)
// ─── C 类:channelDetailedJSON(GetChannelsDetailedJSON)──────
// 当前无 Kotlin 消费方。字段名在此钉住形状。
const (
FieldChannelDetailTopic = "topic"
FieldChannelDetailOrder = "order"
FieldChannelDetailCodec = "codec"
FieldChannelDetailCodecQuality = "codecQuality"
FieldChannelDetailNeededTalkPower = "neededTalkPower"
FieldChannelDetailMaxClients = "maxClients"
FieldChannelDetailMaxFamilyClients = "maxFamilyClients"
FieldChannelDetailIsMaxClientsUnlimited = "isMaxClientsUnlimited"
FieldChannelDetailIsMaxFamilyClientsUnlimited = "isMaxFamilyClientsUnlimited"
FieldChannelDetailIsPermanent = "isPermanent"
FieldChannelDetailIsSemiPermanent = "isSemiPermanent"
FieldChannelDetailIsDefault = "isDefault"
FieldChannelDetailIsOrdered = "isOrdered"
FieldChannelDetailIconID = "iconId"
FieldChannelDetailNeededModifyPower = "neededModifyPower"
)
// ─── C 类:clientDetailedInfoJSON(GetClientDetailInfoJSON)───
// 当前无 Kotlin 消费方。
const (
FieldClientDetailType = "type"
FieldClientDetailAway = "away"
FieldClientDetailAwayMessage = "awayMessage"
FieldClientDetailInputMuted = "inputMuted"
FieldClientDetailOutputMuted = "outputMuted"
FieldClientDetailPlatform = "platform"
FieldClientDetailVersion = "version"
FieldClientDetailIP = "ip"
FieldClientDetailCreated = "created"
FieldClientDetailLastConnected = "lastConnected"
FieldClientDetailTotalConnections = "totalConnections"
FieldClientDetailDescription = "description"
FieldClientDetailIconID = "iconId"
)
// ─── C 类:initialSyncJSON(GetInitialSyncJSON)────────────────
// 一次性返回全部初始数据的聚合契约。当前无 Kotlin 消费方
// (Repository.performInitialSync 走的是三个独立 getter 串行请求)。
const (
FieldSyncChannels = "channels"
FieldSyncClients = "clients"
FieldSyncSelfID = "selfId"
FieldSyncSelfChannelID = "selfChannelId"
FieldSyncServer = "server"
)
// 消息 targetMode 取值。
//
// ⚠️ 收发能力不对称(重要,别再读成三种都支持):
//
// TargetModePrivate(1) 仅接收。发送侧未实现——TSBridge.sendTextMessage 只实现 mode=2,
// 其余返回"暂不支持该消息类型"。SDK 的 SendTextMessage 走的也是
// mode=2 路径,私聊回复需要另找 API。
// TargetModeChannel(2) 收发均支持。
// TargetModeServer(3) 仅接收。
const (
TargetModePrivate = 1
TargetModeChannel = 2
TargetModeServer = 3
)
// GetContractVersion 返回当前 bridge 契约版本。
// Kotlin 侧在建立连接前调用并与本地期望值比对。
// gomobile 不支持返回 error,版本不匹配由 Kotlin 侧抛错处理。
func GetContractVersion() string {
return BridgeContractVersion
}