/** * 消息同步服务 * 处理消息和会话的服务器同步逻辑 * * 重构说明: * - 直接使用 zustand store 替代 IMessageStateManager * - 移除了 SubscriptionManager 调用,Zustand 的 set() 会自动触发组件重渲染 */ import type { ConversationResponse, MessageResponse } from '../../../types/dto'; import { messageService } from '@/services/message'; import { messageRepository, conversationRepository } from '@/database'; import { MessageMapper } from '@/data/mappers'; import { type IConversationListPagedSource, SqliteConversationListPagedSource, createRemoteConversationListSource, CONVERSATION_LIST_PAGE_SIZE, } from '../sources'; import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types'; import { useMessageStore, normalizeConversationId, mergeMessagesById } from '../store'; import { ReadReceiptManager } from './ReadReceiptManager'; export class MessageSyncService implements IMessageSyncService { private getCurrentUserId: () => string | null; private readReceiptManager: ReadReceiptManager; private userCacheService: IUserCacheService; /** 远端会话列表(游标或页码) */ private readonly remoteConversationListSource: IConversationListPagedSource; /** 本地会话列表缓存(SQLite) */ private readonly localConversationListSource: IConversationListPagedSource; /** 正在加载会话列表下一页 */ private loadingMoreConversations = false; /** 聊天页活跃期间延迟的会话列表刷新 */ private deferredConversationRefresh = false; constructor( getCurrentUserId: () => string | null, readReceiptManager: ReadReceiptManager, userCacheService: IUserCacheService, deps?: MessageManagerConversationListDeps ) { this.getCurrentUserId = getCurrentUserId; this.readReceiptManager = readReceiptManager; this.userCacheService = userCacheService; const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE; this.remoteConversationListSource = deps?.remoteConversationListSource ?? createRemoteConversationListSource( deps?.remoteListKind === 'offset' ? 'offset' : 'cursor', pageSize ); this.localConversationListSource = deps?.localConversationListSource ?? new SqliteConversationListPagedSource(); } /** * 获取会话列表 */ async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise { const store = useMessageStore.getState(); if (store.isLoading() && !forceRefresh) { return; } if (this.shouldDeferConversationRefresh(source, forceRefresh)) { this.deferredConversationRefresh = true; if (__DEV__) { console.log('[MessageSyncService] defer fetchConversations', { source, activeConversationId: store.getActiveConversation(), }); } return; } if (__DEV__) { console.log('[MessageSyncService] fetchConversations', { source, forceRefresh, activeConversationId: store.getActiveConversation(), }); } // 非强制刷新且内存为空:先用本地源暖机 const currentState = store.getState(); if (!forceRefresh && currentState.conversations.size === 0) { const warmed = await this.hydrateConversationsFromLocalSource(); if (warmed) { this.persistConversationListCache(); } } store.setLoading(true); try { this.remoteConversationListSource.restart(); const page = await this.remoteConversationListSource.loadNext(); // 使用 zustand set 函数正确更新状态 const newConversations = new Map(); page.items.forEach(conv => { const normalizedConv = this.normalizeConversationFromFetch(conv); newConversations.set(normalizedConv.id, normalizedConv); }); // 使用 store.setConversations 会自动排序并更新 conversationList store.setConversations(newConversations); // 计算并更新未读数 const totalUnread = Array.from(newConversations.values()).reduce( (sum, conv) => sum + (conv.unread_count || 0), 0 ); store.setUnreadCount(totalUnread, store.getUnreadCount().system); this.persistConversationListCache(); } catch (error) { console.error('[MessageSyncService] 获取会话列表失败:', error); if (store.getConversations().length === 0) { const recovered = await this.hydrateConversationsFromLocalSource(); if (recovered) { this.persistConversationListCache(); } } } finally { store.setLoading(false); } } /** * 会话列表加载下一页 */ async loadMoreConversations(): Promise { const store = useMessageStore.getState(); if (!this.remoteConversationListSource.hasMore) { return; } if (this.loadingMoreConversations || store.isLoading()) { return; } this.loadingMoreConversations = true; try { const page = await this.remoteConversationListSource.loadNext(); page.items.forEach(conv => { const normalizedConv = this.normalizeConversationFromFetch(conv); store.updateConversation(normalizedConv); }); this.recomputeConversationTotalUnread(); this.persistConversationListCache(); } catch (error) { console.error('[MessageSyncService] 加载更多会话失败:', error); } finally { this.loadingMoreConversations = false; } } /** * 获取单个会话详情 */ async fetchConversationDetail(conversationId: string): Promise { const store = useMessageStore.getState(); const normalizedId = normalizeConversationId(conversationId); try { const detail = await messageService.getConversationById(normalizedId); if (detail) { const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse); store.updateConversation(normalizedDetail); return normalizedDetail; } return null; } catch (error) { console.error('[MessageSyncService] 获取会话详情失败:', error); return null; } } /** * 获取会话消息(增量同步) */ async fetchMessages(conversationId: string, afterSeq?: number): Promise { const store = useMessageStore.getState(); // 防止重复加载 if (store.isLoadingMessages(conversationId)) { return; } store.setLoadingMessages(conversationId, true); try { const existingMessagesAtStart = store.getMessages(conversationId); const hasInMemoryMessages = existingMessagesAtStart.length > 0; let baselineMaxSeq = hasInMemoryMessages ? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0) : 0; if (!afterSeq) { // 先从本地数据库加载 if (!hasInMemoryMessages) { try { const localMessages = await messageRepository.getByConversation(conversationId, 20); const localMaxSeq = await messageRepository.getMaxSeq(conversationId); baselineMaxSeq = localMaxSeq; if (localMessages.length > 0) { const formattedMessages: MessageResponse[] = localMessages.map(m => ({ id: m.id, conversation_id: m.conversationId, sender_id: m.senderId, seq: m.seq, segments: m.segments || [], status: m.status as any, created_at: m.createdAt, })); store.setMessages(conversationId, formattedMessages); } else { // 冷启动兜底:先设置空列表 store.setMessages(conversationId, []); } } catch (error) { console.warn('[MessageSyncService] 读取本地消息失败:', error); } } // 服务端快照 + 增量同步 try { const snapshotResp = await messageService.getMessages(conversationId, undefined, undefined, 50); const snapshotMessages = snapshotResp?.messages || []; if (snapshotMessages.length > 0) { const existingMessages = store.getMessages(conversationId); const mergedSnapshot = mergeMessagesById(existingMessages, snapshotMessages); store.setMessages(conversationId, mergedSnapshot); messageRepository.saveMessagesBatch( MessageMapper.toCachedMessages(snapshotMessages, conversationId) ).catch(error => { console.error('[MessageSyncService] 保存快照消息到本地失败:', error); }); } // 增量补齐 const snapshotMaxSeq = snapshotMessages.reduce((max, m) => Math.max(max, m.seq || 0), 0); if (snapshotMaxSeq > baselineMaxSeq) { const incrementalResp = await messageService.getMessages(conversationId, baselineMaxSeq); const newMessages = incrementalResp?.messages || []; if (newMessages.length > 0) { const existingMessages = store.getMessages(conversationId); const mergedMessages = mergeMessagesById(existingMessages, newMessages); store.setMessages(conversationId, mergedMessages); messageRepository.saveMessagesBatch( MessageMapper.toCachedMessages(newMessages, conversationId) ).catch(error => { console.error('[MessageSyncService] 保存增量消息到本地失败:', error); }); } } } catch (error) { console.error('[MessageSyncService] 快照/增量同步失败:', error); } } else { // 指定了 afterSeq const response = await messageService.getMessages(conversationId, afterSeq); if (response?.messages && response.messages.length > 0) { const newMessages = response.messages; const existingMessages = store.getMessages(conversationId); const mergedMessages = mergeMessagesById(existingMessages, newMessages); store.setMessages(conversationId, mergedMessages); messageRepository.saveMessagesBatch( MessageMapper.toCachedMessages(newMessages, conversationId) ).catch(error => { console.error('[MessageSyncService] 保存消息到本地失败:', error); }); } } } catch (error) { console.error('[MessageSyncService] 获取消息失败:', error); } finally { // 异步填充用户信息(不阻塞消息显示) const currentMessages = store.getMessages(conversationId); if (currentMessages.length > 0) { this.userCacheService.enrichMessagesWithSenderInfo( conversationId, currentMessages, (convId, enrichedMessages) => { store.setMessages(convId, enrichedMessages); } ); } store.setLoadingMessages(conversationId, false); } } /** * 加载更多历史消息 */ async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise { const store = useMessageStore.getState(); try { // 先从本地获取 const localMessages = await messageRepository.getBeforeSeq(conversationId, beforeSeq, limit); if (localMessages.length >= limit) { const formattedMessages: MessageResponse[] = [...localMessages].reverse().map(m => ({ id: m.id, conversation_id: m.conversationId, sender_id: m.senderId, seq: m.seq, segments: m.segments || [], status: m.status as any, created_at: m.createdAt, })); const existingMessages = store.getMessages(conversationId); const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages); store.setMessages(conversationId, mergedMessages); return formattedMessages; } // 本地数据不足,从服务端获取 const response = await messageService.getMessages(conversationId, undefined, beforeSeq, limit); if (response?.messages && response.messages.length > 0) { const serverMessages = response.messages; await messageRepository.saveMessagesBatch( MessageMapper.toCachedMessages(serverMessages, conversationId) ); const existingMessages = store.getMessages(conversationId); const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages); store.setMessages(conversationId, mergedMessages); return serverMessages; } return []; } catch (error) { console.error('[MessageSyncService] 加载更多消息失败:', error); return []; } } /** * 获取未读数 */ async fetchUnreadCount(): Promise { const store = useMessageStore.getState(); try { const [unreadData, systemUnreadData] = await Promise.all([ messageService.getUnreadCount(), messageService.getSystemUnreadCount(), ]); const totalUnread = unreadData?.total_unread_count ?? 0; const systemUnread = systemUnreadData?.unread_count ?? 0; store.setUnreadCount(totalUnread, systemUnread); // 获取最后一条系统通知的时间 if (systemUnread > 0) { try { const latestSystemMsg = await messageService.getSystemMessagesCursor({ cursor: '', page_size: 1 }); if (latestSystemMsg?.list?.[0]?.created_at) { store.setLastSystemMessageAt(latestSystemMsg.list[0].created_at); } } catch { // 获取最后系统通知时间失败,不影响主流程 } } // 服务端汇总未读为 0 时,清掉内存中残留的红点 if (totalUnread === 0) { const currentConversations = store.getState().conversations; let anyCleared = false; const newConversations = new Map(currentConversations); for (const [cid, conv] of newConversations) { if ((conv.unread_count || 0) > 0) { newConversations.set(cid, { ...conv, unread_count: 0 }); anyCleared = true; } } if (anyCleared) { // 使用 zustand set 函数正确更新状态 store.setConversations(newConversations); this.persistConversationListCache(); } } } catch (error) { console.error('[MessageSyncService] 获取未读数失败:', error); } } /** * 检查是否可加载更多会话 */ canLoadMoreConversations(): boolean { return this.remoteConversationListSource.hasMore; } /** * 重启数据源 */ restartSources(): void { this.remoteConversationListSource.restart(); this.localConversationListSource.restart(); this.loadingMoreConversations = false; } // ==================== 私有工具方法 ==================== private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean { const store = useMessageStore.getState(); if (!store.getActiveConversation()) return false; if (!forceRefresh) return false; // hooks-initial-refresh 是冷启动兜底刷新,不应该被延迟 return source === 'sse-reconnect' || source === 'prefetch'; } private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse { const id = normalizeConversationId(conv.id); // 应用已读保护逻辑 return this.readReceiptManager.applyConversationFromFetch({ ...conv, id, }); } private recomputeConversationTotalUnread(): void { const store = useMessageStore.getState(); const totalUnread = Array.from(store.getState().conversations.values()).reduce( (sum, conv) => sum + (conv.unread_count || 0), 0 ); // 使用 zustand set 函数正确更新状态 store.setUnreadCount(totalUnread, store.getUnreadCount().system); } private persistConversationListCache(): void { const store = useMessageStore.getState(); const currentState = store.getState(); const list = Array.from(currentState.conversations.values()); const users = list.flatMap(conv => [ ...(conv.participants || []), ...(conv.last_message?.sender ? [conv.last_message.sender] : []), ]); const groups = list .map(conv => conv.group) .filter((group): group is NonNullable => Boolean(group)); conversationRepository.saveWithRelated(list, users, groups).catch(error => { console.error('[MessageSyncService] 持久化会话列表失败:', error); }); } private async hydrateConversationsFromLocalSource(): Promise { const store = useMessageStore.getState(); this.localConversationListSource.restart(); const page = await this.localConversationListSource.loadNext(); if (!page.items.length) { return false; } // 使用 zustand set 函数正确更新状态 const newConversations = new Map(); page.items.forEach(conv => { const normalizedConv = this.normalizeConversationFromFetch(conv); newConversations.set(normalizedConv.id, normalizedConv); }); // 使用 store.setConversations 会自动排序并更新 conversationList store.setConversations(newConversations); // 计算并更新未读数 const totalUnread = Array.from(newConversations.values()).reduce( (sum, conv) => sum + (conv.unread_count || 0), 0 ); store.setUnreadCount(totalUnread, store.getUnreadCount().system); return true; } private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] { if (incoming.length === 0) return existing; const incomingAsc = [...incoming].sort((a, b) => a.seq - b.seq); if (existing.length === 0) return incomingAsc; const existingMinSeq = existing[0]?.seq ?? Number.MAX_SAFE_INTEGER; const incomingMaxSeq = incomingAsc[incomingAsc.length - 1]?.seq ?? Number.MIN_SAFE_INTEGER; if (incomingMaxSeq < existingMinSeq) { return [...incomingAsc, ...existing]; } return mergeMessagesById(existing, incomingAsc); } }