# 状态控制设计

## 1. 整体架构

```
┌─────────────────────────────────────────────────────────────────┐
│                        Go Bridge (TSBridge)                      │
│  onConnected / onDisconnected / onTextMessage / onClientEnter    │
│  onClientLeave / onClientMoved / onKicked / onVoiceData / onPoked│
└──────────────────────────────┬──────────────────────────────────┘
                               │ JNI 回调
                               ▼
┌──────────────────────────────────────────────────────────────────┐
│                     ServerViewModel (协调者)                      │
│  connectionState / serverInfo / kickReason / pokeNotification    │
│  拥有其他 ViewModel 引用，分发事件                                  │
└──────┬────────────┬────────────┬────────────┬────────────────────┘
       │            │            │            │
       ▼            ▼            ▼            ▼
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ ChannelVM │ │  ChatVM   │ │ VoiceVM   │ │ Repository│
│ 频道列表   │ │  文字聊天  │ │ 语音通信   │ │ (单例)    │
│ 频道切换   │ │  消息归档  │ │ PTT控制   │ │ 数据仓库   │
│ 未读状态   │ │  送达确认  │ │ 说话检测   │ │           │
└───────────┘ └───────────┘ └───────────┘ └───────────┘
       │            │            │            │
       ▼            ▼            ▼            ▼
┌──────────────────────────────────────────────────────────────────┐
│                      Compose UI (StateFlow collectAsState)        │
│  NavGraph → ChannelListScreen / ChatScreen / KickedScreen         │
└──────────────────────────────────────────────────────────────────┘
```

## 2. Repository（单例数据仓库）

所有 ViewModel 共享的底层数据源。ViewModel 读取 Repository 的 StateFlow，事件处理通过 Repository 方法更新。

```
Repository (object)
├── channels: StateFlow<List<ChannelInfo>>          // 频道列表（低频，按需请求）
├── clients: StateFlow<List<ClientInfo>>             // 客户端列表（高频，人员变动全量刷新）
├── channelClients: StateFlow<Map<String, List<ClientInfo>>>  // 按频道索引（由 clients 派生）
├── selfClientId: StateFlow<Int>                     // 自身客户端 ID
├── currentChannelId: StateFlow<String>              // 当前频道 ID
├── serverInfo: StateFlow<ServerInfo?>               // 服务器信息
├── unreadCounts: StateFlow<Map<String, Int>>        // 未读消息计数
├── activeChatKey: String?                           // 当前查看的会话
└── messageArchives: ConcurrentHashMap               // 消息归档
```

**数据更新频率设计原则**：

| 数据 | 变动频率 | SDK 事件支持 | 更新策略 |
|------|---------|-------------|---------|
| 频道列表 | 极低频 | 无事件 | 连接时全量请求一次，之后按需刷新（超5分钟过期检测） |
| 客户端列表 | 高频 | Enter/Leave/Move | 每次事件全量刷新 `refreshClientList()` |
| 频道树 | — | — | 不维护中间状态，UI 层直接从 channels + clients 实时计算 |

**关键方法**：
- `performInitialSync()` — 首次同步（串行请求频道列表、客户端列表、自身 ID）
- `refreshClientList()` — 全量刷新客户端列表（人员变动时调用，高频）
- `updateBaseline()` — 原子提交基线数据
- `clearSession()` — 清理会话（断开连接时调用）
- `archiveMessage()` — 归档消息
- `confirmMessageDelivery()` — 匹配回显确认送达

## 3. ViewModel 状态明细

### 3.1 ServerViewModel — 连接与事件协调

**职责**：管理连接生命周期、接收 Go Bridge 回调并分发给其他 ViewModel。

```
ServerViewModel
├── state: StateFlow<ServerScreenState>
│   ├── address / nickname / password    // 输入字段
│   ├── connectState: ConnectState       // 连接按钮状态
│   │   ├── IDLE         空闲，等待用户输入
│   │   ├── CONNECTING   连接中
│   │   ├── SUCCESS      连接成功（触发导航）
│   │   ├── FAILED       连接失败
│   │   └── TIMEOUT      连接超时
│   └── errorMessage / validationErrors
│
├── connectionState: StateFlow<ConnectionState?>
│   ├── null             未进入频道列表页
│   ├── Connected        已连接
│   ├── Disconnecting    断开中
│   ├── Reconnecting     重连中（attempt, reason）
│   └── Disconnected     已断开（reason, wasKicked）
│
├── serverInfo: StateFlow<ServerInfo?>
├── kickReason: StateFlow<String>
├── pokeNotification / showPokeNotification
├── themeMode: StateFlow<ThemeMode>
└── recents: StateFlow<List<RecentConnection>>
```

**Bridge 回调分发**：

| 回调 | 分发目标 |
|------|---------|
| `onConnected` | `channelViewModel.performInitialSync()` → 导航到频道列表 |
| `onDisconnected` | `Repository.clearSession()` + `channelViewModel.clearChannels()` + `chatViewModel.clearMessages()` |
| `onTextMessage` | `chatViewModel.handleTextMessage()` + `channelViewModel.onTextMessage()` |
| `onClientEnter` | `channelViewModel.handleClientEnter()` |
| `onClientLeave` | `channelViewModel.handleClientLeave()` |
| `onClientMoved` | `channelViewModel.handleClientMoved()` |
| `onKicked` | `Repository.clearSession()` + 所有 ViewModel 清理 → 导航到 KickedScreen |
| `onVoiceData` | `voiceViewModel.handleVoiceData()` |
| `onPoked` | `serverViewModel.handlePoked()` |

**ViewModel 引用**（由 NavGraph LaunchedEffect 设置）：
```
serverViewModel.channelViewModel = channelViewModel
serverViewModel.chatViewModel = chatViewModel
serverViewModel.voiceViewModel = voiceViewModel
```

### 3.2 ChannelViewModel — 频道列表与切换

**职责**：管理同步状态、频道切换状态机、展开状态、未读指示。频道树不在 ViewModel 中维护，由 UI 层实时计算。

```
ChannelViewModel
├── syncState: StateFlow<SyncState>
│   ├── Unsynced       已连接但尚无完整数据（初始状态）
│   ├── Syncing        正在调用 ListChannels / ListClients
│   ├── Synchronized   列表基线可供 UI 使用
│   └── SyncFailed     同步失败
│
├── channels: StateFlow<List<ChannelInfo>>              // Repository 引用（低频）
├── clients: StateFlow<List<ClientInfo>>                // Repository 引用（高频）
├── channelClients: StateFlow<Map<String, List<ClientInfo>>>  // Repository 引用
├── selfClientId: StateFlow<Int>                        // Repository 引用
├── currentChannelId: StateFlow<String>                 // Repository 引用
│
├── expandedChannelIds: StateFlow<Set<String>>          // 本地状态（初始同步时自动展开有成员的频道）
├── hasUnreadMessage: StateFlow<Boolean>                // 本地状态（当前频道外有新消息）
│
├── switchState: StateFlow<ChannelSwitchState>
│   ├── Idle                  空闲
│   ├── Requesting            正在发送 ClientMove
│   ├── WaitingServerEvent    等待 OnClientMoved 确认
│   └── Failed                切换失败
│
├── showPasswordDialog / pendingSwitchChannel
└── channelDetailInfo / showChannelDetailCard
```

**频道树构建（UI 层）**：
UI 直接从源 StateFlow 实时计算，不维护中间状态：
```kotlin
// ChannelListScreen
val tree = remember(channels, clients, expandedChannelIds) {
    channelViewModel.buildChannelTree(channels, clients, expandedChannelIds, unreadStates)
}
```

**频道数据更新策略**：
- 频道列表是静态数据，SDK 不提供频道增删改事件
- 连接时全量请求一次（`performInitialSync`），之后按需刷新（超5分钟过期检测）
- 人员变动事件（Enter/Leave/Move）只刷新客户端列表，不碰频道列表
- 如果管理员创建/删除了频道，用户下次切换频道时自动检测过期并刷新

**频道切换状态机**：
```
用户点击频道
    │
    ▼
  Idle ──→ Requesting ──→ WaitingServerEvent ──→ Idle
    │           │               │
    │           ▼               ▼
    │        Failed          超时 → Failed
    │           │
    └───────────┘
       用户重试/取消
```

**事件处理**：

| 事件 | 处理 |
|------|------|
| `handleClientEnter()` | `Repository.refreshClientList()` 全量刷新（高频） |
| `handleClientLeave()` | `Repository.refreshClientList()` 全量刷新（高频） |
| `handleClientMoved()` | 区分自己/他人，全量刷新 + 更新 currentChannelId（高频） |
| `onTextMessage()` | 设置 hasUnreadMessage 标记（仅当 targetId != currentChannelId） |

### 3.3 ChatViewModel — 文字聊天

**职责**：管理消息列表、发送状态、送达确认。

```
ChatViewModel
├── messages: StateFlow<List<ChatMessage>>      // 当前会话消息
├── sendState: StateFlow<MessageSendState>
│   ├── Idle          空闲
│   ├── Sending       发送中
│   └── Failed        发送失败
│
├── currentTargetMode: Int      // 1=私聊, 2=频道, 3=服务器
├── currentTargetId: Long       // 目标 ID
└── deliveryTimeoutJob          // 送达确认超时
```

**消息生命周期**：
```
用户输入
    │
    ▼
sendMessage()
    │
    ├── 1. 创建 PENDING 消息 → Repository.archiveMessage()
    ├── 2. TSBridge.sendTextMessage()
    │       ├── 成功 → 等待回显（10秒超时）
    │       └── 失败 → 标记 FAILED
    │
    ▼
收到 OnTextMessage 回显
    │
    ├── Repository.confirmMessageDelivery() 匹配 PENDING
    │       ├── 匹配 → 标记 SENT（送达确认）
    │       └── 不匹配 → 正常归档（他人消息）
    │
    ▼
超时未确认 → 标记 FAILED
```

**会话管理**：
- `enterChat()` — 设置 activeChatKey，加载消息，清除未读
- `leaveChat()` — 清除 activeChatKey，重置发送状态
- `clearMessages()` — 清空消息列表（断开连接时）

### 3.4 VoiceViewModel — 语音通信

**职责**：管理 PTT、静音、语音状态、说话检测。

```
VoiceViewModel
├── voiceState: StateFlow<VoiceState>
│   ├── Idle            空闲（可 PTT）
│   ├── Transmitting    发送中（PTT 按下）
│   └── Blocked         受阻（原因：未连接/未入频道/权限/断开）
│
├── isMuted: StateFlow<Boolean>             // 默认 true
├── isTransmitting: StateFlow<Boolean>
├── speakerEnabled: StateFlow<Boolean>
├── denoiseEnabled: StateFlow<Boolean>
├── showVoiceCard: StateFlow<Boolean>
├── outputDevice: StateFlow<VoiceOutputDevice>
├── inputVolume / outputVolume: StateFlow<Float>
├── isSelfSpeaking: StateFlow<Boolean>
└── speakingClients: StateFlow<Map<Int, Long>>
```

**PTT 状态机**：
```
Idle ──→ startTransmit() ──→ Transmitting ──→ stopTransmit() ──→ Idle
  │                                │
  │ 前置检查失败                     │ 异常
  ▼                                ▼
Blocked ←──────────────────────────┘
  │
  └── clearBlocked() ──→ Idle
```

## 4. 状态生命周期

### 4.1 连接建立

```
用户点击连接
    │
    ▼
ServerViewModel.connect()
    │  connectState = CONNECTING
    ▼
TSBridge.connect() (IO 线程)
    │
    ▼
onConnected 回调
    │
    ├── Repository 记录连接
    ├── ChannelVM.performInitialSync()
    │       ├── syncState = Syncing
    │       ├── Repository.performInitialSync() (串行请求)
    │       ├── syncState = Synchronized
    │       └── autoExpandChannelsWithClients()
    │
    ├── ConnectionService.start() (前台服务保活)
    ├── connectState = SUCCESS
    └── connectionState = Connected
            │
            ▼
        NavGraph 导航到 CHANNEL_LIST
```

### 4.2 正常断开

```
用户点击断开
    │
    ▼
ServerViewModel.disconnect()
    │
    ├── ConnectionService.stop()
    ├── reconnectJob?.cancel()
    ├── connectionState = Disconnecting
    ├── voiceViewModel.stopVoice()
    ├── TSBridge.disconnect()
    │
    ├── Repository.clearSession()
    │       ├── channels/clients = empty
    │       ├── selfClientId = 0
    │       ├── currentChannelId = "0"
    │       ├── channelClients = empty
    │       └── messageArchives.clear()
    │
    ├── channelViewModel.clearChannels()
    │       ├── syncState = Unsynced
    │       ├── expandedChannelIds = empty
    │       ├── unreadStates = empty
    │       └── switchState = Idle
    │
    ├── chatViewModel.clearMessages()
    │       ├── messages = empty
    │       └── sendState = Idle
    │
    ├── connectionState = null
    └── connectState = IDLE
            │
            ▼
        NavGraph 导航到 SERVER_CONFIG
```

### 4.3 被踢出

```
onKicked 回调
    │
    ├── ConnectionService.stop()
    ├── reconnectJob?.cancel()
    ├── voiceViewModel.stopVoice()
    ├── Repository.clearSession()
    ├── channelViewModel.clearChannels()
    │
    ├── kickReason = reason
    └── connectionState = Disconnected(wasKicked=true)
            │
            ▼
        NavGraph 导航到 KICKED
```

### 4.4 网络断开（自动重连已禁用）

```
onDisconnected 回调
    │
    ├── 检查 connectionState
    │   ├── Disconnecting → 忽略（主动断开）
    │   └── 其他 → 继续
    │
    ├── voiceViewModel.onDisconnected()
    │       └── voiceState = Blocked("连接已断开")
    │
    ├── ConnectionService.stop()
    ├── Repository.clearSession()
    ├── channelViewModel.clearChannels()
    │
    └── connectionState = Disconnected(wasKicked=false)
            │
            ▼
        NavGraph 导航到 SERVER_CONFIG
```

## 5. 状态流向图

```
                    ┌──────────────────────────────┐
                    │        Repository            │
                    │  channels (低频) / clients (高频) │
                    └──────┬───────────────────────┘
                           │ StateFlow（只读引用）
              ┌────────────┼────────────────┐
              ▼            ▼                ▼
     ┌────────────┐ ┌──────────┐    ┌──────────┐
     │ ChannelVM  │ │  ChatVM  │    │ VoiceVM  │
     │ 同步状态    │ │ 消息管理  │    │ 语音控制  │
     │ 切换状态机  │ │ 送达确认  │    │ PTT      │
     │ 展开/未读   │ │          │    │          │
     └─────┬──────┘ └────┬─────┘    └────┬─────┘
           │ StateFlow    │ StateFlow     │ StateFlow
           ▼              ▼               ▼
     ┌─────────────────────────────────────────┐
     │           Compose UI Layer              │
     │  collectAsState() → remember 派生计算    │
     └─────────────────────────────────────────┘
```

**数据流规则**：
1. **Repository** 持有底层数据。`channels` 低频更新（连接时请求），`clients` 高频更新（每次人员变动全量刷新）
2. **ViewModel** 读取 Repository 的 StateFlow 引用，仅管理本地 UI 状态（expandedChannelIds、hasUnreadMessage、switchState 等）
3. **UI** 通过 `collectAsState()` 订阅 StateFlow，使用 `remember` 实时派生计算（频道树）。不维护中间 combine 状态
4. **事件** 从 ServerViewModel（Bridge 回调）→ ViewModel 方法 → Repository 更新 → StateFlow 自动通知 UI

## 6. 导航状态控制

NavGraph 监听 `connectionState` 变化，自动处理导航：

```
connectionState 变化
    │
    ├── Disconnected(wasKicked=true)  → navigate(KICKED)
    ├── Disconnected(wasKicked=false) → navigate(SERVER_CONFIG)
    ├── Connected (当前在 KICKED)      → navigate(CHANNEL_LIST)
    └── 其他                           → 不处理
```

**页面与 ViewModel 生命周期**：
- ViewModel 作用域绑定到 NavBackStackEntry
- 从 CHANNEL_LIST 导航到 CHAT 时，ChannelVM 不销毁（在返回栈中）
- 从 CHAT 返回 CHANNEL_LIST 时，ChannelVM 状态保持
- 如果系统回收 ViewModel（内存不足），`LaunchedEffect` 检测 `syncState != Synchronized` 并重新触发同步

## 7. 已知状态问题与防护

| 问题 | 防护措施 |
|------|---------|
| ViewModel 被系统回收后 syncState 回到 Unsynced | ChannelListScreen 的 `LaunchedEffect` 自动重新同步 |
| 详情按钮快速连点 | ViewModel 层 500ms 防抖 |
| `leaveChat()` 被调用多次（Compose 重组） | `leaveChat()` 本身是幂等操作，无副作用 |
