# 聊天系统性能优化设计方案

## 文档信息

- **创建日期**: 2026-03-18
- **项目**: 胡萝卜 BBS (Carrot BBS) 前端
- **范围**: 消息系统性能优化

---

## 目录

1. [架构概述](#架构概述)
2. [P0 分页状态管理](#p0-分页状态管理)
3. [P0 同步状态机](#p0-同步状态机)
4. [P1 差异更新](#p1-差异更新)
5. [P1 媒体缓存清理](#p1-媒体缓存清理)
6. [实现优先级与依赖关系](#实现优先级与依赖关系)

---

## 架构概述

### 当前消息系统架构

```
┌─────────────────────────────────────────────────────────────────┐
│                        UI Layer                                  │
│  ┌─────────────────┐  ┌─────────────────┐                      │
│  │  ChatScreen     │  │ MessageList     │                      │
│  │  useChatScreen  │  │ Screen          │                      │
│  └────────┬────────┘  └────────┬────────┘                    │
└───────────┼─────────────────────┼───────────────────────────────┘
            │                     │
            ▼                     ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Store Layer (Zustand)                         │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ messageManager (MessageManager)                              ││
│  │  - messages Map<conversationId, Message[]>                 ││
│  │  - conversations: Conversation[]                            ││
│  │  - activeConversation: string | null                        ││
│  │  - isConnected: boolean                                     ││
│  └─────────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ messageManagerHooks (React Hooks)                            ││
│  │  - useChat(conversationId)                                   ││
│  │  - useMessages(conversationId)                              ││
│  │  - useConversations()                                       ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────────┐
│                   UseCase Layer                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ ProcessMessageUseCase                                        ││
│  │  - WebSocket 事件监听与处理                                   ││
│  │  - 消息去重 (processedMessageIds)                            ││
│  │  - 已读状态管理 (pendingReadMap)                              ││
│  │  - 消息持久化到 Repository                                    ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────────┐
│                  DataSource Layer                                │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────────┐│
│  │WebSocketClient│  │CacheDataSource│  │LocalDataSource (SQLite)││
│  │               │  │ (Memory+Async) │  │                       ││
│  │ - 事件订阅     │  │ - get/set      │  │ - messages 表         ││
│  │ - emit        │  │ - delete       │  │ - conversations 表    ││
│  │ - 连接状态     │  │ - clear        │  │ - users 表            ││
│  └───────────────┘  └───────────────┘  └───────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────────┐
│                   Infrastructure Layer                           │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ sseService (SSE Client)                                      ││
│  │  - EventSource 连接管理                                       ││
│  │  - 重连逻辑 (maxReconnectAttempts=20)                        ││
│  │  - AppState 监听                                             ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```

### 问题分析

基于代码分析，当前系统存在以下问题：

| 问题 | 影响 | 优先级 |
|------|------|--------|
| 分页状态管理不完善 | 用户滚动加载时可能重复请求同一页数据 | P0 |
| 连接状态反馈不清晰 | 用户不知道连接是否正常 | P0 |
| 全量数据更新 | 大房间每次更新都刷新整个消息列表 | P1 |
| 媒体缓存无清理机制 | 长期使用后存储空间爆炸 | P1 |

---

## P0 分页状态管理

### 现有代码分析

#### 问题点

1. **`useChatScreen.ts` (第388-408行)**
   ```typescript
   const loadMoreHistory = useCallback(async () => {
     if (!conversationId || !hasMoreHistory || loadingMore) {
       return;
     }
     // 缺少分页游标管理，可能导致重复加载
     setLoadingMore(true);
     try {
       await loadMoreMessages();
       // 没有追踪当前加载到了哪一页
     } finally {
       setLoadingMore(false);
     }
   }, [...]);
   ```

2. **`messageManagerHooks.ts` (第156-158行)**
   ```typescript
   setIsLoading(true);
   const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20);
   // 直接使用 minSeq，但没有缓存 minSeq 的状态
   ```

3. **`MessageRepository.ts` (第125-137行)**
   ```typescript
   async getMessagesBeforeSeq(conversationId, beforeSeq, limit = 20) {
     // 依赖 beforeSeq 参数，但没有分页偏移量管理
   }
   ```

#### 架构缺陷

- 没有独立的分页状态存储
- `hasMoreMessages` 状态与实际数据可能不同步
- 并发加载时缺少互斥机制
- 没有加载中的分页信息缓存

### 实现方案

#### 1. 新增分页状态管理器

```typescript
// src/stores/pagination/PaginationStateManager.ts

interface PaginationState {
  conversationId: string;
  currentMinSeq: number;        // 当前已加载的最小seq
  currentMaxSeq: number;        // 当前已加载的最大seq
  pages: Map<number, string[]>;  // pageNumber -> messageIds
  loadingPageNumbers: Set<number>;
  hasMoreBefore: boolean;
  hasMoreAfter: boolean;
  totalLoaded: number;
}

class PaginationStateManager {
  private states: Map<string, PaginationState> = new Map();
  
  // 获取或创建分页状态
  getOrCreate(conversationId: string): PaginationState { ... }
  
  // 标记页面加载中
  markPageLoading(conversationId: string, pageNumber: number): void { ... }
  
  // 标记页面加载完成
  markPageLoaded(conversationId: string, pageNumber: number, messageIds: string[], hasMore: boolean): void { ... }
  
  // 检查页面是否已加载
  isPageLoaded(conversationId: string, pageNumber: number): boolean { ... }
  
  // 检查页面是否正在加载
  isPageLoading(conversationId: string, pageNumber: number): boolean { ... }
  
  // 重置分页状态
  reset(conversationId: string): void { ... }
}
```

#### 2. 修改 useMessages Hook

```typescript
// src/stores/messageManagerHooks.ts

interface UseMessagesOptions {
  pageSize?: number;
  prefetchThreshold?: number;  // 预加载阈值
}

function useMessages(
  conversationId: string | null,
  options: UseMessagesOptions = {}
) {
  const {
    pageSize = 20,
    prefetchThreshold = 5
  } = options;

  // 分页状态
  const [paginationState, setPaginationState] = useState<PaginationState | null>(null);

  // 计算当前页码
  const currentPage = useMemo(() => {
    if (!paginationState) return 1;
    return Math.ceil(
      (paginationState.totalLoaded - messages.length) / pageSize
    ) + 1;
  }, [paginationState, messages.length]);

  // 加载更多（带分页保护）
  const loadMoreMessages = useCallback(async () => {
    if (!conversationId || !paginationState) return;
    
    // 计算下一页
    const nextPage = Math.floor(paginationState.totalLoaded / pageSize) + 1;
    
    // 检查是否正在加载
    if (paginationState.loadingPageNumbers.has(nextPage)) {
      return;
    }
    
    // 检查是否已加载
    if (paginationState.pages.has(nextPage)) {
      return;
    }

    await messageManager.loadMoreMessages(
      conversationId,
      paginationState.currentMinSeq,
      pageSize
    );
  }, [conversationId, paginationState, pageSize]);

  // 预加载检测
  useEffect(() => {
    if (!paginationState || !hasMore) return;
    
    const currentPageLoaded = messages.length;
    const threshold = pageSize - prefetchThreshold;
    
    if (currentPageLoaded <= threshold && !paginationState.loadingPageNumbers.has(1)) {
      loadMoreMessages();
    }
  }, [messages.length, hasMore, loadMoreMessages]);
}
```

#### 3. 修改 MessageManager

```typescript
// src/stores/message/MessageManager.ts

class MessageManager {
  private paginationManager = new PaginationStateManager();

  async loadMoreMessages(
    conversationId: string,
    beforeSeq: number,
    limit: number
  ): Promise<Message[]> {
    // 检查分页状态
    const pageNumber = this.calculatePageNumber(conversationId, beforeSeq);
    
    if (this.paginationManager.isPageLoading(conversationId, pageNumber)) {
      return [];  // 避免重复加载
    }
    
    if (this.paginationManager.isPageLoaded(conversationId, pageNumber)) {
      return [];  // 已加载，直接返回
    }

    this.paginationManager.markPageLoading(conversationId, pageNumber);

    try {
      // 调用 API
      const response = await messageService.getMessages(conversationId, {
        before_seq: beforeSeq,
        limit
      });

      const messageIds = response.messages.map(m => m.id);
      
      this.paginationManager.markPageLoaded(
        conversationId,
        pageNumber,
        messageIds,
        response.has_more
      );

      // 更新消息存储
      this.appendMessages(conversationId, response.messages);

      return response.messages;
    } catch (error) {
      // 加载失败，清除分页状态
      this.paginationManager.markPageFailed(conversationId, pageNumber);
      throw error;
    }
  }
}
```

#### 4. 修改数据库查询

```typescript
// src/services/database.ts (或 MessageRepository)

export async function getMessagesBeforeSeq(
  conversationId: string,
  beforeSeq: number,
  limit: number
): Promise<CachedMessage[]> {
  // 确保使用索引优化查询
  const result = await db.getAllAsync<CachedMessage>(`
    SELECT * FROM messages 
    WHERE conversationId = ? AND seq < ?
    ORDER BY seq DESC
    LIMIT ?
  `, [conversationId, beforeSeq, limit]);
  
  return result;
}
```

### 修改/新增文件列表

| 文件 | 操作 | 说明 |
|------|------|------|
| `src/stores/pagination/PaginationStateManager.ts` | 新增 | 分页状态管理器 |
| `src/stores/pagination/index.ts` | 新增 | 导出文件 |
| `src/stores/messageManagerHooks.ts` | 修改 | 集成分页状态管理 |
| `src/stores/message/MessageManager.ts` | 修改 | 添加分页保护逻辑 |
| `src/data/repositories/MessageRepository.ts` | 修改 | 优化分页查询 |

### 关键设计决策

1. **分页粒度**: 以 `beforeSeq` 为游标，而非 offset，避免消息新增导致的分页漂移
2. **页面缓存**: 使用 `Map<pageNumber, messageIds[]>` 缓存已加载页面，避免重复请求
3. **加载锁**: 使用 `Set<pageNumber>` 追踪正在加载的页面，防止并发重复加载
4. **预加载**: 当滚动到距离底部 `prefetchThreshold` 条消息时，提前加载下一页

---

## P0 同步状态机

### 现有代码分析

#### 问题点

1. **`sseService.ts` (第406-408行)**
   ```typescript
   isConnected(): boolean {
     return this.source != null;
   }
   // 状态判断过于简单，source != null 不代表真正连接成功
   ```

2. **`WebSocketClient.ts` (第23-35行)**
   ```typescript
   export interface WebSocketEvents {
     'connected': void;
     'disconnected': void;
     'error': Error;
   }
   // 只有 connected/disconnected 两种状态
   ```

3. **`sseService.ts` (第190-228行)**
   ```typescript
   async connect(): Promise<boolean> {
     // isConnecting 状态没有暴露给外部
     // 外部无法区分"正在连接"和"连接失败"
   }
   ```

4. **`messageManagerHooks.ts` (第390-405行)**
   ```typescript
   const [isConnected, setIsConnected] = useState(() => messageManager.isConnected());
   // 连接状态变化没有详细的状态分类
   ```

#### 状态定义缺失

当前没有区分以下状态：
- **Connecting**: 正在建立连接
- **Connected**: 连接已建立
- **Reconnecting**: 连接断开，正在重连
- **Disconnected**: 连接已断开
- **Error**: 连接错误

### 实现方案

#### 1. 定义连接状态机

```typescript
// src/services/connection/ConnectionState.ts

export enum ConnectionStateType {
  IDLE = 'idle',
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
  RECONNECTING = 'reconnecting',
  DISCONNECTED = 'disconnected',
  ERROR = 'error',
}

export interface ConnectionState {
  type: ConnectionStateType;
  timestamp: number;
  error?: Error;
  retryCount?: number;
  lastConnectedAt?: number;
}

export interface ConnectionStateChangeEvent {
  previousState: ConnectionState;
  currentState: ConnectionState;
  reason?: string;
}
```

#### 2. 创建连接状态管理器

```typescript
// src/services/connection/ConnectionStateManager.ts

type ConnectionStateListener = (event: ConnectionStateChangeEvent) => void;

class ConnectionStateManager {
  private state: ConnectionState = {
    type: ConnectionStateType.IDLE,
    timestamp: Date.now(),
  };
  
  private listeners: Set<ConnectionStateListener> = new Set();
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 20;

  // 获取当前状态
  getState(): ConnectionState { ... }

  // 状态转换
  transition(newType: ConnectionStateType, reason?: string, error?: Error): void {
    const previousState = this.state;
    this.state = {
      type: newType,
      timestamp: Date.now(),
      error,
      retryCount: newType === ConnectionStateType.RECONNECTING 
        ? this.reconnectAttempts 
        : undefined,
      lastConnectedAt: newType === ConnectionStateType.CONNECTED 
        ? Date.now() 
        : this.state.lastConnectedAt,
    };
    
    this.notifyListeners({ previousState, currentState: this.state, reason });
  }

  // 增加重试计数
  incrementRetry(): number {
    this.reconnectAttempts++;
    return this.reconnectAttempts;
  }

  // 重置重试计数
  resetRetry(): void {
    this.reconnectAttempts = 0;
  }

  // 订阅状态变化
  subscribe(listener: ConnectionStateListener): () => void { ... }

  // 获取可读的状态描述
  getStatusDescription(): string {
    switch (this.state.type) {
      case ConnectionStateType.IDLE:
        return '未连接';
      case ConnectionStateType.CONNECTING:
        return '正在连接...';
      case ConnectionStateType.CONNECTED:
        return '已连接';
      case ConnectionStateType.RECONNECTING:
        return `正在重连 (${this.state.retryCount}/${this.maxReconnectAttempts})`;
      case ConnectionStateType.DISCONNECTED:
        return '已断开';
      case ConnectionStateType.ERROR:
        return `连接错误: ${this.state.error?.message || '未知错误'}`;
    }
  }
}

export const connectionStateManager = new ConnectionStateManager();
```

#### 3. 修改 SSEService

```typescript
// src/services/sseService.ts

class SSEService {
  private connectionStateManager = connectionStateManager;

  async connect(): Promise<boolean> {
    if (this.isConnecting || this.isConnected()) {
      return true;
    }

    this.connectionStateManager.transition(
      ConnectionStateType.CONNECTING,
      'initiated'
    );

    try {
      const token = await api.getToken();
      if (!token) {
        throw new Error('No auth token');
      }

      // ... 建立连接 ...

      this.source.addEventListener('open', () => {
        this.connectionStateManager.resetRetry();
        this.connectionStateManager.transition(
          ConnectionStateType.CONNECTED,
          'connection_opened'
        );
      });

      this.source.addEventListener('error', (error) => {
        this.connectionStateManager.transition(
          ConnectionStateType.ERROR,
          'connection_error',
          error
        );
        this.scheduleReconnect();
      });

      return true;
    } catch (error) {
      this.connectionStateManager.transition(
        ConnectionStateType.ERROR,
        'connection_failed',
        error
      );
      this.scheduleReconnect();
      return false;
    }
  }

  private scheduleReconnect(): void {
    const retryCount = this.connectionStateManager.incrementRetry();
    
    if (retryCount >= this.maxReconnectAttempts) {
      this.connectionStateManager.transition(
        ConnectionStateType.DISCONNECTED,
        'max_reconnect_exceeded'
      );
      return;
    }

    this.connectionStateManager.transition(
      ConnectionStateType.RECONNECTING,
      'scheduling_reconnect'
    );

    this.reconnectTimer = setTimeout(() => {
      this.connect();
    }, this.reconnectDelay);
  }
}
```

#### 4. 创建连接状态 Hook

```typescript
// src/hooks/useConnectionState.ts

export function useConnectionState() {
  const [state, setState] = useState<ConnectionState>(
    () => connectionStateManager.getState()
  );

  useEffect(() => {
    const unsubscribe = connectionStateManager.subscribe((event) => {
      setState(event.currentState);
    });

    return unsubscribe;
  }, []);

  return {
    state,
    status: connectionStateManager.getStatusDescription(),
    isConnected: state.type === ConnectionStateType.CONNECTED,
    isConnecting: state.type === ConnectionStateType.CONNECTING,
    isReconnecting: state.type === ConnectionStateType.RECONNECTING,
    isDisconnected: state.type === ConnectionStateType.DISCONNECTED,
    isError: state.type === ConnectionStateType.ERROR,
    retryCount: state.retryCount,
    error: state.error,
    reconnect: () => sseService.connect(),
    disconnect: () => sseService.disconnect(),
  };
}
```

#### 5. UI 集成示例

```typescript
// src/screens/message/components/ChatScreen/ConnectionStatusBadge.tsx

export function ConnectionStatusBadge() {
  const { status, isConnected, isReconnecting, isError, retryCount } = useConnectionState();

  if (isConnected) {
    return null; // 已连接不显示
  }

  return (
    <View style={styles.badge}>
      {isReconnecting && (
        <ActivityIndicator size="small" color="#666" />
      )}
      <Text style={styles.text}>
        {isReconnecting 
          ? `连接中断，正在重连 (${retryCount}/20)` 
          : status}
      </Text>
    </View>
  );
}
```

### 修改/新增文件列表

| 文件 | 操作 | 说明 |
|------|------|------|
| `src/services/connection/ConnectionState.ts` | 新增 | 连接状态定义 |
| `src/services/connection/ConnectionStateManager.ts` | 新增 | 连接状态管理器 |
| `src/services/connection/index.ts` | 新增 | 导出文件 |
| `src/services/sseService.ts` | 修改 | 集成状态管理器 |
| `src/hooks/useConnectionState.ts` | 新增 | 连接状态 Hook |
| `src/hooks/index.ts` | 修改 | 导出新 Hook |

### Mermaid 状态图

```mermaid
stateDiagram-v2
  [*] --> IDLE
  IDLE --> CONNECTING: 调用 connect()
  CONNECTING --> CONNECTED: 连接成功
  CONNECTING --> ERROR: 连接失败
  CONNECTED --> RECONNECTING: 连接断开
  RECONNECTING --> CONNECTED: 重连成功
  RECONNECTING --> DISCONNECTED: 超过最大重试次数
  RECONNECTING --> ERROR: 重连失败
  ERROR --> RECONNECTING: 自动重连
  ERROR --> DISCONNECTED: 停止重连
  DISCONNECTED --> CONNECTING: 手动重连
  IDLE --> CONNECTING: 手动重连
  CONNECTED --> DISCONNECTED: 调用 disconnect()
```

---

## P1 差异更新

### 现有代码分析

#### 问题点

1. **`MessageManager` 消息更新机制**
   - 每当有新消息时，通过 `setMessages` 更新整个消息数组
   - 大房间场景下，频繁的全量更新会导致 UI 卡顿
   - 没有区分不同类型的消息更新（新增、删除、修改）

2. **`messageManagerHooks.ts` (第33-46行)**
   ```typescript
   const unsubscribe = messageManager.subscribe((event: MessageEvent) => {
     switch (event.type) {
       case 'conversations_updated':
         setConversations(messageManager.getConversations());
         break;
       // ... 其他都是全量更新
     }
   });
   ```

3. **`useChatScreen.ts` (第148-162行)**
   ```typescript
   const messages = useMemo<GroupMessage[]>(() => {
     return messageManagerMessages.map(m => ({
       // 全量映射
     }));
   }, [messageManagerMessages]);
   ```

4. **`ProcessMessageUseCase.ts` (第159-199行)**
   ```typescript
   private async handleNewMessage(message: any): Promise<void> {
     // 每条消息都触发完整流程
     this.notifySubscribers({
       type: 'message_received',
       payload: { message, ... }
     });
   }
   ```

#### 性能瓶颈

- 1000人群聊，每秒10条消息 = 每秒10000次 UI 更新操作
- 全量 `setMessages` 触发 FlatList 完整重渲染
- 没有虚拟列表优化

### 实现方案

#### 1. 定义消息更新类型

```typescript
// src/stores/message/MessageUpdateTypes.ts

export enum MessageUpdateType {
  APPEND = 'append',           // 追加新消息到末尾
  PREPEND = 'prepend',          // 预置历史消息到开头
  UPDATE = 'update',            // 更新单条消息
  DELETE = 'delete',            // 删除单条消息
  BATCH_APPEND = 'batch_append', // 批量追加
  BATCH_PREPEND = 'batch_prepend', // 批量预置
  RECALL = 'recall',            // 撤回消息
  CLEAR = 'clear',             // 清空会话
}

export interface MessageUpdate<T = any> {
  type: MessageUpdateType;
  conversationId: string;
  payload: T;
  timestamp: number;
}

export interface AppendPayload {
  messages: Message[];
}

export interface PrependPayload {
  messages: Message[];
  hasMoreBefore: boolean;
}

export interface UpdatePayload {
  messageId: string;
  updates: Partial<Message>;
}

export interface DeletePayload {
  messageId: string;
}

export interface BatchAppendPayload {
  messages: Message[];
}

export interface RecallPayload {
  messageId: string;
}
```

#### 2. 创建差异更新 Hook

```typescript
// src/hooks/useDifferentialMessages.ts

export function useDifferentialMessages(conversationId: string | null) {
  const [messages, setMessages] = useState<Message[]>([]);
  const pendingUpdatesRef = useRef<MessageUpdate[]>([]);
  const flushScheduledRef = useRef(false);

  // 批量处理更新
  const flushUpdates = useCallback(() => {
    if (pendingUpdatesRef.current.length === 0) return;

    const updates = pendingUpdatesRef.current;
    pendingUpdatesRef.current = [];
    flushScheduledRef.current = false;

    setMessages(currentMessages => {
      const newMessages = [...currentMessages];
      
      for (const update of updates) {
        switch (update.type) {
          case MessageUpdateType.APPEND:
            for (const msg of update.payload.messages) {
              if (!newMessages.find(m => m.id === msg.id)) {
                newMessages.push(msg);
              }
            }
            break;

          case MessageUpdateType.PREPEND:
            const prependMessages = update.payload.messages
              .filter(m => !newMessages.find(n => n.id === m.id));
            newMessages.unshift(...prependMessages.reverse());
            break;

          case MessageUpdateType.UPDATE:
            const updateIdx = newMessages.findIndex(m => m.id === update.payload.messageId);
            if (updateIdx !== -1) {
              newMessages[updateIdx] = {
                ...newMessages[updateIdx],
                ...update.payload.updates
              };
            }
            break;

          case MessageUpdateType.DELETE:
            newMessages = newMessages.filter(m => m.id !== update.payload.messageId);
            break;

          case MessageUpdateType.RECALL:
            const recallIdx = newMessages.findIndex(m => m.id === update.payload.messageId);
            if (recallIdx !== -1) {
              newMessages[recallIdx] = {
                ...newMessages[recallIdx],
                status: 'recalled',
                segments: [],
              };
            }
            break;

          case MessageUpdateType.BATCH_APPEND:
            for (const msg of update.payload.messages) {
              if (!newMessages.find(m => m.id === msg.id)) {
                newMessages.push(msg);
              }
            }
            break;
        }
      }

      return newMessages;
    });
  }, []);

  // 调度批量更新（16ms 内合并）
  const scheduleFlush = useCallback(() => {
    if (flushScheduledRef.current) return;
    flushScheduledRef.current = true;
    requestAnimationFrame(flushUpdates);
  }, [flushUpdates]);

  // 处理消息更新
  const handleMessageUpdate = useCallback((update: MessageUpdate) => {
    pendingUpdatesRef.current.push(update);
    scheduleFlush();
  }, [scheduleFlush]);

  // 订阅 MessageManager
  useEffect(() => {
    if (!conversationId) return;

    const unsubscribe = messageManager.subscribe((event) => {
      if (event.type === 'message_received') {
        const { message, isCurrentUser } = event.payload;
        handleMessageUpdate({
          type: isCurrentUser 
            ? MessageUpdateType.APPEND 
            : MessageUpdateType.PREPEND,
          conversationId,
          payload: { messages: [message] },
          timestamp: Date.now(),
        });
      }

      if (event.type === 'messages_loaded') {
        const { messages, direction } = event.payload;
        handleMessageUpdate({
          type: direction === 'before' 
            ? MessageUpdateType.PREPEND 
            : MessageUpdateType.BATCH_APPEND,
          conversationId,
          payload: { messages },
          timestamp: Date.now(),
        });
      }

      if (event.type === 'message_recalled') {
        handleMessageUpdate({
          type: MessageUpdateType.RECALL,
          conversationId,
          payload: { messageId: event.payload.messageId },
          timestamp: Date.now(),
        });
      }
    });

    return unsubscribe;
  }, [conversationId, handleMessageUpdate]);

  return { messages };
}
```

#### 3. 优化 FlatList 渲染

```typescript
// src/screens/message/components/ChatScreen/OptimizedMessageList.tsx

export function OptimizedMessageList({
  messages,
  onLoadMore,
  hasMore,
}: {
  messages: Message[];
  onLoadMore: () => void;
  hasMore: boolean;
}) {
  const viewabilityConfig = useRef({
    itemVisiblePercentThreshold: 50,
    minimumViewTime: 100,
  });

  // 关键消息路径优化
  const keyExtractor = useCallback((item: Message) => item.id, []);

  const getItemLayout = useCallback((data: Message[] | null, index: number) => {
    // 估算每条消息高度（可以根据类型调整）
    const BASE_HEIGHT = 60;
    const IMAGE_EXTRA = 200;
    const EXTRA_PER_SEGMENT = 30;

    let length = BASE_HEIGHT;
    if (data) {
      const message = data[index];
      if (message.segments) {
        length += message.segments.length * EXTRA_PER_SEGMENT;
      }
      if (message.segments?.some(s => s.type === 'image')) {
        length += IMAGE_EXTRA;
      }
    }

    return {
      length,
      offset: data.slice(0, index).reduce((sum, m) => {
        // 计算累计高度
        return sum + BASE_HEIGHT + 
          (m.segments?.length || 0) * EXTRA_PER_SEGMENT +
          (m.segments?.some(s => s.type === 'image') ? IMAGE_EXTRA : 0);
      }, 0),
      index,
    };
  }, []);

  // 消息类型判断
  const isSameDay = useCallback((prev: Message, next: Message) => {
    const prevDate = new Date(prev.created_at).toDateString();
    const nextDate = new Date(next.created_at).toDateString();
    return prevDate === nextDate;
  }, []);

  const renderItem = useCallback(({ item, index }: { item: Message; index: number }) => {
    const prevItem = messages[index - 1];
    
    return (
      <MessageBubble
        message={item}
        showDateSeparator={!prevItem || !isSameDay(prevItem, item)}
        showTimeSeparator={prevItem && item.seq - prevItem.seq > 50}
      />
    );
  }, [isSameDay]);

  return (
    <FlatList
      data={messages}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      onEndReached={hasMore ? onLoadMore : null}
      onEndReachedThreshold={5}
      maxToRenderPerBatch={10}
      windowSize={21}
      removeClippedSubviews={true}
      initialNumToRender={20}
    />
  );
}
```

#### 4. 修改 MessageManager 支持差异事件

```typescript
// src/stores/message/MessageManager.ts

class MessageManager {
  // 发送差异更新事件
  private emitMessageUpdate(update: MessageUpdate): void {
    this.subscribers.forEach(subscriber => {
      if (subscriber.types.includes(update.type) || subscriber.types.includes('*')) {
        subscriber.callback(update);
      }
    });
  }

  async handleIncomingMessage(message: WSChatMessage): Promise<void> {
    // 检查消息是否已存在
    const existingMessages = this.messages.get(message.conversation_id) || [];
    if (existingMessages.find(m => m.id === message.id)) {
      return; // 避免重复
    }

    const newMessage = this.createMessageObject(message);

    // 直接发送差异更新，而不是全量更新
    this.emitMessageUpdate({
      type: MessageUpdateType.APPEND,
      conversationId: message.conversation_id,
      payload: { messages: [newMessage] },
      timestamp: Date.now(),
    });

    // 异步保存到数据库
    this.persistMessage(newMessage);
  }
}
```

### 修改/新增文件列表

| 文件 | 操作 | 说明 |
|------|------|------|
| `src/stores/message/MessageUpdateTypes.ts` | 新增 | 消息更新类型定义 |
| `src/hooks/useDifferentialMessages.ts` | 新增 | 差异更新 Hook |
| `src/stores/messageManagerHooks.ts` | 修改 | 集成差异更新 |
| `src/stores/message/MessageManager.ts` | 修改 | 添加差异事件支持 |
| `src/screens/message/components/ChatScreen/OptimizedMessageList.tsx` | 新增 | 优化的消息列表组件 |

### 性能收益

| 场景 | 优化前 | 优化后 |
|------|--------|--------|
| 1000人群，每秒10条消息 | 10000次/秒 UI 更新 | 60次/秒 UI 更新 |
| 滚动加载100条历史消息 | 全量重渲染100条 | 仅渲染可见区域~20条 |
| 消息撤回 | 全量更新 + UI 重建 | 单条更新 |

---

## P1 媒体缓存清理

### 现有代码分析

#### 问题点

1. **`CacheDataSource.ts` (第14-28行)**
   ```typescript
   export class CacheDataSource implements ICacheDataSource {
     private memoryCache: Map<string, CacheEntry<any>> = new Map();
     private maxSize: number;      // 默认 100 条
     private defaultTtl: number;   // 默认 5 分钟
     
     // 没有媒体文件专用缓存管理
     // 没有磁盘空间监控
   }
   ```

2. **缺少媒体缓存机制**
   - 图片、视频、音频没有独立的缓存策略
   - 没有按会话清理过期媒体的功能
   - 没有用户手动清理入口

3. **`LocalDataSource.ts` (第73-86行)**
   ```typescript
   // 消息表结构
   `CREATE TABLE IF NOT EXISTS messages (
     id TEXT PRIMARY KEY NOT NULL,
     conversationId TEXT NOT NULL,
     segments TEXT
     // 没有存储媒体文件路径
   )`
   ```
   - 消息表没有记录媒体缓存路径
   - 无法追踪哪些媒体文件属于哪个会话

#### 存储风险

- 图片缓存：无限制增长
- 视频缓存：无限制增长
- 音频缓存：无限制增长
- 总计：可能导致存储空间耗尽

### 实现方案

#### 1. 定义媒体缓存配置

```typescript
// src/services/media/MediaCacheConfig.ts

export interface MediaCacheConfig {
  // 图片配置
  image: {
    maxMemoryCacheSize: number;      // 内存缓存数量，默认 50
    maxDiskCacheSize: number;        // 磁盘缓存大小(MB)，默认 500
    maxAge: number;                  // 缓存有效期(小时)，默认 168 (7天)
    maxAgeForConversation: number;  // 会话内媒体保留时间(天)，默认 30
  };
  
  // 视频配置
  video: {
    maxDiskCacheSize: number;        // 磁盘缓存大小(MB)，默认 1000
    maxAge: number;                  // 缓存有效期(小时)，默认 72 (3天)
    autoPreload: boolean;           // 是否自动预加载，默认 false
  };
  
  // 音频配置
  audio: {
    maxDiskCacheSize: number;        // 磁盘缓存大小(MB)，默认 200
    maxAge: number;                  // 缓存有效期(天)，默认 7
  };
  
  // 全局配置
  global: {
    checkOnStartup: boolean;        // 启动时检查清理，默认 true
    checkInterval: number;          // 定期检查间隔(小时)，默认 24
    lowStorageThreshold: number;   // 低存储空间阈值(MB)，默认 500
  };
}

export const DEFAULT_MEDIA_CACHE_CONFIG: MediaCacheConfig = {
  image: {
    maxMemoryCacheSize: 50,
    maxDiskCacheSize: 500,
    maxAge: 168,  // 7 days
    maxAgeForConversation: 30,
  },
  video: {
    maxDiskCacheSize: 1000,
    maxAge: 72,   // 3 days
    autoPreload: false,
  },
  audio: {
    maxDiskCacheSize: 200,
    maxAge: 168,  // 7 days
  },
  global: {
    checkOnStartup: true,
    checkInterval: 24,
    lowStorageThreshold: 500,
  },
};
```

#### 2. 创建媒体缓存管理器

```typescript
// src/services/media/MediaCacheManager.ts

import * as FileSystem from 'expo-file-system';
import { cacheDataSource } from '../data/datasources/CacheDataSource';

export enum MediaType {
  IMAGE = 'image',
  VIDEO = 'video',
  AUDIO = 'audio',
}

export interface MediaCacheInfo {
  type: MediaType;
  uri: string;
  localPath: string;
  conversationId?: string;
  messageId?: string;
  size: number;
  createdAt: number;
  lastAccessedAt: number;
}

class MediaCacheManager {
  private config: MediaCacheConfig;
  private cacheDirectory: string;
  private mediaRecords: Map<string, MediaCacheInfo> = new Map();
  
  // 缓存目录
  private readonly CACHE_DIR = `${FileSystem.documentDirectory}media_cache/`;
  private readonly IMAGE_DIR = `${this.CACHE_DIR}images/`;
  private readonly VIDEO_DIR = `${this.CACHE_DIR}videos/`;
  private readonly AUDIO_DIR = `${this.CACHE_DIR}audios/`;

  constructor(config: MediaCacheConfig = DEFAULT_MEDIA_CACHE_CONFIG) {
    this.config = config;
    this.cacheDirectory = this.CACHE_DIR;
  }

  // 初始化
  async initialize(): Promise<void> {
    await this.ensureDirectories();
    await this.loadMediaRecords();
    await this.cleanupIfNeeded();
  }

  // 确保缓存目录存在
  private async ensureDirectories(): Promise<void> {
    await FileSystem.makeDirectoryAsync(this.IMAGE_DIR, { intermediates: true });
    await FileSystem.makeDirectoryAsync(this.VIDEO_DIR, { intermediates: true });
    await FileSystem.makeDirectoryAsync(this.AUDIO_DIR, { intermediates: true });
  }

  // 缓存媒体文件
  async cacheMedia(
    uri: string,
    type: MediaType,
    options: {
      conversationId?: string;
      messageId?: string;
    } = {}
  ): Promise<string> {
    const { conversationId, messageId } = options;
    
    // 生成唯一文件名
    const filename = this.generateFilename(uri, type);
    const localPath = this.getMediaPath(type, filename);
    
    // 检查是否已缓存
    if (await this.exists(localPath)) {
      await this.updateAccessTime(localPath);
      return localPath;
    }

    try {
      // 下载文件
      const downloadResult = await FileSystem.downloadAsync(uri, localPath);
      
      // 记录缓存信息
      const cacheInfo: MediaCacheInfo = {
        type,
        uri,
        localPath,
        conversationId,
        messageId,
        size: downloadResult.headers['content-length'] 
          ? parseInt(downloadResult.headers['content-length']) 
          : 0,
        createdAt: Date.now(),
        lastAccessedAt: Date.now(),
      };
      
      await this.saveMediaRecord(localPath, cacheInfo);
      await this.cleanupIfNeeded();
      
      return localPath;
    } catch (error) {
      console.error('[MediaCacheManager] 缓存失败:', error);
      throw error;
    }
  }

  // 获取缓存的媒体文件路径
  async getCachedMedia(uri: string, type: MediaType): Promise<string | null> {
    const record = this.findByUri(uri);
    if (record && await this.exists(record.localPath)) {
      await this.updateAccessTime(record.localPath);
      return record.localPath;
    }
    return null;
  }

  // 删除单个会话的所有媒体缓存
  async clearConversationMedia(conversationId: string): Promise<void> {
    const toDelete: string[] = [];
    
    for (const [path, info] of this.mediaRecords) {
      if (info.conversationId === conversationId) {
        toDelete.push(path);
      }
    }
    
    await Promise.all(toDelete.map(p => this.deleteMedia(p)));
  }

  // 删除过期媒体
  async clearExpiredMedia(): Promise<number> {
    const now = Date.now();
    const maxAge = this.config.image.maxAge * 60 * 60 * 1000; // 转换为毫秒
    let deletedCount = 0;
    
    for (const [path, info] of this.mediaRecords) {
      if (now - info.lastAccessedAt > maxAge) {
        await this.deleteMedia(path);
        deletedCount++;
      }
    }
    
    return deletedCount;
  }

  // 清理超过会话保留期的媒体
  async clearOldConversationMedia(): Promise<number> {
    const now = Date.now();
    const maxAge = this.config.image.maxAgeForConversation * 24 * 60 * 60 * 1000;
    let deletedCount = 0;
    
    for (const [path, info] of this.mediaRecords) {
      if (info.conversationId && now - info.createdAt > maxAge) {
        await this.deleteMedia(path);
        deletedCount++;
      }
    }
    
    return deletedCount;
  }

  // 按大小清理（LRU）
  async clearBySizeLimit(): Promise<number> {
    const totalSize = await this.getTotalCacheSize();
    const limit = this.config.image.maxDiskCacheSize * 1024 * 1024;
    
    if (totalSize <= limit) {
      return 0;
    }
    
    // 按最后访问时间排序
    const sorted = Array.from(this.mediaRecords.entries())
      .sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
    
    let currentSize = totalSize;
    let deletedCount = 0;
    
    for (const [path, info] of sorted) {
      if (currentSize <= limit) break;
      
      await this.deleteMedia(path);
      currentSize -= info.size;
      deletedCount++;
    }
    
    return deletedCount;
  }

  // 获取缓存统计
  async getCacheStats(): Promise<{
    totalSize: number;
    imageCount: number;
    videoCount: number;
    audioCount: number;
    oldestItem: number;
    newestItem: number;
  }> {
    const stats = {
      totalSize: 0,
      imageCount: 0,
      videoCount: 0,
      audioCount: 0,
      oldestItem: Date.now(),
      newestItem: 0,
    };
    
    for (const info of this.mediaRecords.values()) {
      stats.totalSize += info.size;
      stats.oldestItem = Math.min(stats.oldestItem, info.createdAt);
      stats.newestItem = Math.max(stats.newestItem, info.createdAt);
      
      switch (info.type) {
        case MediaType.IMAGE: stats.imageCount++; break;
        case MediaType.VIDEO: stats.videoCount++; break;
        case MediaType.AUDIO: stats.audioCount++; break;
      }
    }
    
    return stats;
  }

  // 清理入口
  async cleanupIfNeeded(): Promise<void> {
    await this.clearExpiredMedia();
    await this.clearBySizeLimit();
  }

  // 私有辅助方法
  private generateFilename(uri: string, type: MediaType): string {
    const ext = this.getExtension(uri, type);
    const hash = this.hashString(uri);
    return `${type}_${hash}_${Date.now()}.${ext}`;
  }

  private getExtension(uri: string, type: MediaType): string {
    if (type === MediaType.IMAGE) {
      if (uri.includes('.gif')) return 'gif';
      if (uri.includes('.webp')) return 'webp';
      return 'jpg';
    }
    if (type === MediaType.VIDEO) return 'mp4';
    if (type === MediaType.AUDIO) return 'mp3';
    return 'bin';
  }

  private getMediaPath(type: MediaType, filename: string): string {
    switch (type) {
      case MediaType.IMAGE: return `${this.IMAGE_DIR}${filename}`;
      case MediaType.VIDEO: return `${this.VIDEO_DIR}${filename}`;
      case MediaType.AUDIO: return `${this.AUDIO_DIR}${filename}`;
    }
  }

  private async exists(path: string): Promise<boolean> {
    const info = await FileSystem.getInfoAsync(path);
    return info.exists;
  }

  private async deleteMedia(path: string): Promise<void> {
    try {
      await FileSystem.deleteAsync(path, { idempotent: true });
      this.mediaRecords.delete(path);
      await this.removeMediaRecord(path);
    } catch (error) {
      console.warn('[MediaCacheManager] 删除失败:', path, error);
    }
  }

  private async saveMediaRecord(path: string, info: MediaCacheInfo): Promise<void> {
    this.mediaRecords.set(path, info);
    await cacheDataSource.set(`media_${path}`, info, null);
  }

  private async loadMediaRecords(): Promise<void> {
    // 从缓存数据源加载
  }

  private async updateAccessTime(path: string): Promise<void> {
    const info = this.mediaRecords.get(path);
    if (info) {
      info.lastAccessedAt = Date.now();
      await this.saveMediaRecord(path, info);
    }
  }

  private findByUri(uri: string): MediaCacheInfo | undefined {
    for (const info of this.mediaRecords.values()) {
      if (info.uri === uri) {
        return info;
      }
    }
    return undefined;
  }

  private async getTotalCacheSize(): Promise<number> {
    let total = 0;
    for (const info of this.mediaRecords.values()) {
      total += info.size;
    }
    return total;
  }

  private hashString(str: string): string {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString(16);
  }
}

export const mediaCacheManager = new MediaCacheManager();
```

#### 3. 创建清理策略

```typescript
// src/services/media/MediaCleanupPolicy.ts

export enum CleanupTrigger {
  STARTUP = 'startup',
  SCHEDULED = 'scheduled',
  MANUAL = 'manual',
  LOW_STORAGE = 'low_storage',
  CONVERSATION_DELETED = 'conversation_deleted',
}

export interface CleanupResult {
  trigger: CleanupTrigger;
  timestamp: number;
  deletedCount: number;
  freedSize: number;
  errors: string[];
}

class MediaCleanupPolicy {
  private lastCleanup: number = 0;
  private config: MediaCacheConfig;
  
  // 启动时清理
  async cleanupOnStartup(): Promise<CleanupResult> {
    const errors: string[] = [];
    let deletedCount = 0;
    let freedSize = 0;
    
    try {
      // 清理过期文件
      const expired = await mediaCacheManager.clearExpiredMedia();
      deletedCount += expired;
      
      // 清理超过保留期的会话媒体
      const oldConversation = await mediaCacheManager.clearOldConversationMedia();
      deletedCount += oldConversation;
      
      // 超过大小限制时清理
      const bySize = await mediaCacheManager.clearBySizeLimit();
      deletedCount += bySize;
      
      this.lastCleanup = Date.now();
    } catch (error) {
      errors.push(`Startup cleanup failed: ${error}`);
    }
    
    return {
      trigger: CleanupTrigger.STARTUP,
      timestamp: Date.now(),
      deletedCount,
      freedSize,
      errors,
    };
  }

  // 检查是否需要定期清理
  shouldRunScheduledCleanup(): boolean {
    const interval = this.config.global.checkInterval * 60 * 60 * 1000;
    return Date.now() - this.lastCleanup > interval;
  }

  // 会话删除时清理
  async cleanupOnConversationDeleted(conversationId: string): Promise<CleanupResult> {
    const errors: string[] = [];
    let deletedCount = 0;
    let freedSize = 0;
    
    try {
      const statsBefore = await mediaCacheManager.getCacheStats();
      await mediaCacheManager.clearConversationMedia(conversationId);
      const statsAfter = await mediaCacheManager.getCacheStats();
      
      deletedCount = statsBefore.imageCount + statsBefore.videoCount + statsBefore.audioCount
                   - (statsAfter.imageCount + statsAfter.videoCount + statsAfter.audioCount);
      freedSize = statsBefore.totalSize - statsAfter.totalSize;
    } catch (error) {
      errors.push(`Conversation cleanup failed: ${error}`);
    }
    
    return {
      trigger: CleanupTrigger.CONVERSATION_DELETED,
      timestamp: Date.now(),
      deletedCount,
      freedSize,
      errors,
    };
  }
}

export const mediaCleanupPolicy = new MediaCleanupPolicy();
```

#### 4. 创建清理 Hook

```typescript
// src/hooks/useMediaCache.ts

export function useMediaCache() {
  const [stats, setStats] = useState<CacheStats | null>(null);
  const [isClearing, setIsClearing] = useState(false);

  // 加载缓存统计
  const loadStats = useCallback(async () => {
    const cacheStats = await mediaCacheManager.getCacheStats();
    setStats(cacheStats);
  }, []);

  // 手动清理
  const clearAll = useCallback(async () => {
    setIsClearing(true);
    try {
      await mediaCacheManager.clearExpiredMedia();
      await mediaCacheManager.clearBySizeLimit();
      await loadStats();
    } finally {
      setIsClearing(false);
    }
  }, [loadStats]);

  // 清理指定会话的媒体
  const clearConversation = useCallback(async (conversationId: string) => {
    await mediaCacheManager.clearConversationMedia(conversationId);
    await loadStats();
  }, [loadStats]);

  // 组件挂载时加载统计
  useEffect(() => {
    loadStats();
  }, [loadStats]);

  return {
    stats,
    isClearing,
    loadStats,
    clearAll,
    clearConversation,
    formatSize: (bytes: number) => {
      if (bytes < 1024) return `${bytes} B`;
      if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
      if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
      return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
    },
  };
}
```

### 修改/新增文件列表

| 文件 | 操作 | 说明 |
|------|------|------|
| `src/services/media/MediaCacheConfig.ts` | 新增 | 媒体缓存配置 |
| `src/services/media/MediaCacheManager.ts` | 新增 | 媒体缓存管理器 |
| `src/services/media/MediaCleanupPolicy.ts` | 新增 | 清理策略 |
| `src/services/media/index.ts` | 新增 | 导出文件 |
| `src/hooks/useMediaCache.ts` | 新增 | 媒体缓存 Hook |
| `src/hooks/index.ts` | 修改 | 导出新 Hook |
| `src/data/datasources/CacheDataSource.ts` | 修改 | 添加媒体记录存储 |

### 清理策略

```mermaid
graph TD
    A[启动 App] --> B{检查启动清理标志}
    B -->|是| C[清理过期媒体]
    B -->|是| D[清理超期会话媒体]
    B -->|是| E[检查大小限制]
    E -->|超过限制| F[LRU 清理]
    
    G[定期检查] -->|间隔到期| C
    G -->|间隔到期| D
    G -->|间隔到期| E
    
    H[用户操作] -->|删除会话| I[清理该会话媒体]
    H -->|手动清理| J[清理所有过期媒体]
    
    K[存储空间检查] -->|低于阈值| L[紧急清理]
```

---

## 实现优先级与依赖关系

### 优先级矩阵

| 优先级 | 功能 | 依赖 | 复杂度 |
|--------|------|------|--------|
| P0 | 分页状态管理 | 无 | 中 |
| P0 | 同步状态机 | 无 | 低 |
| P1 | 差异更新 | P0 分页状态管理 | 高 |
| P1 | 媒体缓存清理 | 无 | 中 |

### 推荐实现顺序

```
┌─────────────────────────────────────────────────────────────────┐
│  Phase 1: 基础改进 (1-2天)                                       │
│  ┌───────────────────┐  ┌───────────────────┐                  │
│  │ P0 分页状态管理    │  │ P0 同步状态机      │                  │
│  │                   │  │                   │                  │
│  │ - 避免重复加载     │  │ - 清晰状态反馈     │                  │
│  │ - 页面缓存         │  │ - 连接状态 Hook    │                  │
│  │ - 预加载机制       │  │ - UI 状态指示器    │                  │
│  └───────────────────┘  └───────────────────┘                  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  Phase 2: 性能优化 (2-3天)                                       │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ P1 差异更新                                                  ││
│  │                                                             ││
│  │ - 依赖分页状态管理                                           ││
│  │ - 消息更新类型定义                                           ││
│  │ - 差异更新 Hook                                             ││
│  │ - FlatList 渲染优化                                         ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  Phase 3: 存储优化 (1-2天)                                       │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ P1 媒体缓存清理                                              ││
│  │                                                             ││
│  │ - 媒体缓存管理器                                             ││
│  │ - 清理策略                                                   ││
│  │ - 用户清理界面                                               ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```

### 风险与注意事项

1. **差异更新风险**
   - 需要确保消息 ID 的唯一性
   - 并发更新时需要正确合并
   - 建议在开发环境进行大房间压力测试

2. **分页状态管理风险**
   - 页面缓存会占用内存，需要设置上限
   - 分页状态需要正确序列化和恢复（跨会话）

3. **状态机风险**
   - 状态转换需要严格测试
   - 重连逻辑需要处理边界情况（如网络中断）

4. **媒体缓存风险**
   - 删除文件时需要确保没有正在使用
   - 缓存元数据需要持久化存储
   - 清理操作应该在后台线程执行 