工程改进:跨语言契约固化、日志脱敏、状态模型收敛
依据 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>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user