refactor(message): replace MessageStateManager with zustand store
Some checks failed
Frontend CI / ota-android (push) Successful in 11m15s
Frontend CI / build-and-push-web (push) Successful in 29m13s
Frontend CI / build-android-apk (push) Has been cancelled

- Remove MessageStateManager wrapper layer in favor of direct zustand store
- Move service modules to services/ subdirectory (ConversationOperations, MessageDeduplication, MessageSendService, MessageSyncService, ReadReceiptManager, UserCacheService, WSMessageHandler)
- Add new zustand-based store with subscriptionManager for event handling
- Introduce React hooks for message state (useConversations, useMessages, useUnreadCount, etc.)
- Update exports in index.ts to reflect new module structure
- Deprecate messageManager.ts entry point in favor of message/index.ts
- Maintain backward compatibility with existing MessageManager API
This commit is contained in:
lafay
2026-03-31 18:22:24 +08:00
parent 1bee7ea551
commit 94c11062f0
15 changed files with 1488 additions and 706 deletions

View File

@@ -0,0 +1,266 @@
/**
* 会话操作服务
* 处理会话的 CRUD 操作
*
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
*/
import type { ConversationResponse } from '../../../types/dto';
import { messageService } from '../../../services/messageService';
import { deleteConversation } from '../../../services/database';
import type { IConversationOperations } from '../types';
import { useMessageStore, subscriptionManager, normalizeConversationId } from '../store';
export class ConversationOperations implements IConversationOperations {
/**
* 创建私聊会话
*/
async createConversation(userId: string): Promise<ConversationResponse | null> {
const store = useMessageStore.getState();
try {
const conversation = await messageService.createConversation(userId);
// 添加到会话列表
store.updateConversation(conversation);
// 通知更新
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
return conversation;
} catch (error) {
console.error('[ConversationOperations] 创建会话失败:', error);
subscriptionManager.notifySubscribers({
type: 'error',
payload: { error, context: 'createConversation' },
timestamp: Date.now(),
});
return null;
}
}
/**
* 本地更新会话信息
*/
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation) {
console.warn('[ConversationOperations] 更新会话失败,会话不存在:', conversationId);
return;
}
const updatedConv: ConversationResponse = {
...conversation,
...updates,
};
store.updateConversation(updatedConv);
// 通知更新
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
}
/**
* 仅自己删除会话后的本地状态回收
*/
removeConversation(conversationId: string): void {
const store = useMessageStore.getState();
store.removeConversation(conversationId);
// 通知更新
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
const unreadCount = store.getUnreadCount();
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: unreadCount.total,
systemUnreadCount: unreadCount.system,
},
timestamp: Date.now(),
});
// 删除本地数据库中的会话
deleteConversation(conversationId).catch(error => {
console.error('[ConversationOperations] 删除本地会话失败:', error);
});
}
/**
* 清空会话列表
*/
clearConversations(): void {
const store = useMessageStore.getState();
store.reset();
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: [] },
timestamp: Date.now(),
});
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: 0,
systemUnreadCount: 0,
},
timestamp: Date.now(),
});
}
/**
* 更新单个会话未读数
*/
updateUnreadCount(conversationId: string, count: number): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation) {
console.warn('[ConversationOperations] 更新未读数失败,会话不存在:', conversationId);
return;
}
const prevUnreadCount = conversation.unread_count || 0;
const diff = count - prevUnreadCount;
// 更新会话未读数
const updatedConv: ConversationResponse = {
...conversation,
unread_count: count,
};
store.updateConversation(updatedConv);
// 更新总未读数
const currentUnread = store.getUnreadCount();
store.setUnreadCount(
Math.max(0, currentUnread.total + diff),
currentUnread.system
);
// 通知更新
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: store.getUnreadCount().total,
systemUnreadCount: store.getUnreadCount().system,
},
timestamp: Date.now(),
});
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
}
/**
* 增加会话未读数
*/
incrementUnreadCount(conversationId: string): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation) return;
const prevUnreadCount = conversation.unread_count || 0;
const newUnreadCount = prevUnreadCount + 1;
// 创建新对象而不是直接修改,确保状态一致性
const updatedConv: ConversationResponse = {
...conversation,
unread_count: newUnreadCount,
};
store.updateConversation(updatedConv);
const currentUnread = store.getUnreadCount();
store.setUnreadCount(
currentUnread.total + 1,
currentUnread.system
);
// 通知更新
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: store.getUnreadCount().total,
systemUnreadCount: store.getUnreadCount().system,
},
timestamp: Date.now(),
});
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
}
/**
* 设置系统消息未读数
*/
setSystemUnreadCount(count: number): void {
const store = useMessageStore.getState();
const currentUnread = store.getUnreadCount();
store.setUnreadCount(currentUnread.total, count);
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: currentUnread.total,
systemUnreadCount: count,
},
timestamp: Date.now(),
});
}
/**
* 增加系统消息未读数
*/
incrementSystemUnreadCount(): void {
const store = useMessageStore.getState();
const currentUnread = store.getUnreadCount();
store.setUnreadCount(currentUnread.total, currentUnread.system + 1);
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: currentUnread.total,
systemUnreadCount: currentUnread.system + 1,
},
timestamp: Date.now(),
});
}
/**
* 减少系统消息未读数
*/
decrementSystemUnreadCount(count = 1): void {
const store = useMessageStore.getState();
const currentUnread = store.getUnreadCount();
const newSystemCount = Math.max(0, currentUnread.system - count);
store.setUnreadCount(currentUnread.total, newSystemCount);
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: currentUnread.total,
systemUnreadCount: newSystemCount,
},
timestamp: Date.now(),
});
}
}

View File

@@ -0,0 +1,61 @@
/**
* 消息去重服务
* 处理消息去重逻辑,防止重复处理相同的消息
*/
import type { IMessageDeduplication } from '../types';
import {
PROCESSED_MESSAGE_ID_EXPIRY,
PROCESSED_MESSAGE_ID_MAX_SIZE,
} from '../constants';
export class MessageDeduplication implements IMessageDeduplication {
// 已处理消息ID集合用于去重防止ACK消息和正常消息重复处理
private processedMessageIds: Set<string> = new Set();
private processedMessageIdsExpiry: Map<string, number> = new Map();
/**
* 检查消息是否已处理
*/
isMessageProcessed(messageId: string): boolean {
return this.processedMessageIds.has(messageId);
}
/**
* 标记消息已处理
*/
markMessageAsProcessed(messageId: string): void {
this.processedMessageIds.add(messageId);
this.processedMessageIdsExpiry.set(messageId, Date.now());
// 定期清理
if (this.processedMessageIds.size > PROCESSED_MESSAGE_ID_MAX_SIZE) {
this.cleanupProcessedMessageIds();
}
}
/**
* 清理过期的消息ID防止内存泄漏
*/
cleanupProcessedMessageIds(): void {
const now = Date.now();
for (const [id, timestamp] of this.processedMessageIdsExpiry.entries()) {
if (now - timestamp > PROCESSED_MESSAGE_ID_EXPIRY) {
this.processedMessageIds.delete(id);
this.processedMessageIdsExpiry.delete(id);
}
}
}
/**
* 重置所有已处理的消息ID
* 用于测试或需要清除缓存的场景
*/
reset(): void {
this.processedMessageIds.clear();
this.processedMessageIdsExpiry.clear();
}
}
// 单例导出
export const messageDeduplication = new MessageDeduplication();

View File

@@ -0,0 +1,136 @@
/**
* 消息发送服务
* 处理消息发送相关逻辑
*
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
*/
import type { MessageResponse, MessageSegment, ConversationResponse } from '../../../types/dto';
import { messageService } from '../../../services/messageService';
import { saveMessage } from '../../../services/database';
import type { IMessageSendService } from '../types';
import { useMessageStore, subscriptionManager, mergeMessagesById } from '../store';
export class MessageSendService implements IMessageSendService {
private getCurrentUserId: () => string | null;
constructor(getCurrentUserId: () => string | null) {
this.getCurrentUserId = getCurrentUserId;
}
/**
* 发送消息
*/
async sendMessage(
conversationId: string,
segments: MessageSegment[],
options?: { replyToId?: string }
): Promise<MessageResponse | null> {
const store = useMessageStore.getState();
try {
// 调用API发送
const response = await messageService.sendMessage(conversationId, {
segments,
reply_to_id: options?.replyToId,
});
if (response) {
const currentUserId = this.getCurrentUserId();
// 添加到本地消息列表
const existingMessages = store.getMessages(conversationId);
const newMessage: MessageResponse = {
id: response.id,
conversation_id: conversationId,
sender_id: currentUserId || '',
seq: response.seq,
segments,
created_at: new Date().toISOString(),
status: 'normal',
};
const updatedMessages = mergeMessagesById(existingMessages, [newMessage]);
store.setMessages(conversationId, updatedMessages);
// 关键:立即广播消息列表更新
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: updatedMessages,
newMessage,
source: 'send',
},
timestamp: Date.now(),
});
// 更新会话最后消息
this.updateConversationWithNewMessage(conversationId, newMessage);
// 通知消息已发送
subscriptionManager.notifySubscribers({
type: 'message_sent',
payload: { conversationId, message: newMessage },
timestamp: Date.now(),
});
// 保存到本地数据库
const textContent = segments
?.filter((s: any) => s.type === 'text')
.map((s: any) => s.data?.text || '')
.join('') || '';
saveMessage({
id: response.id,
conversationId,
senderId: currentUserId || '',
content: textContent,
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
isRead: true,
createdAt: newMessage.created_at,
seq: response.seq,
status: 'normal',
segments,
}).catch(console.error);
return newMessage;
}
return null;
} catch (error) {
console.error('[MessageSendService] 发送消息失败:', error);
subscriptionManager.notifySubscribers({
type: 'error',
payload: { error, context: 'sendMessage' },
timestamp: Date.now(),
});
return null;
}
}
/**
* 更新会话的最后消息信息
*/
private updateConversationWithNewMessage(
conversationId: string,
message: MessageResponse
): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
const createdAt = message.created_at || new Date().toISOString();
if (conversation) {
// 更新现有会话
const updatedConv: ConversationResponse = {
...conversation,
last_seq: message.seq,
last_message: message,
last_message_at: createdAt,
updated_at: createdAt,
};
store.updateConversation(updatedConv);
}
}
}

View File

@@ -0,0 +1,681 @@
/**
* 消息同步服务
* 处理消息和会话的服务器同步逻辑
*
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
*/
import type { ConversationResponse, MessageResponse } from '../../../types/dto';
import { messageService } from '../../../services/messageService';
import {
getMessagesByConversation,
getMaxSeq,
saveMessagesBatch,
saveConversationsWithRelatedCache,
getMessagesBeforeSeq,
} from '../../../services/database';
import {
type IConversationListPagedSource,
SqliteConversationListPagedSource,
createRemoteConversationListSource,
CONVERSATION_LIST_PAGE_SIZE,
} from '../../conversationListSources';
import type { IMessageSyncService, MessageManagerConversationListDeps, IUserCacheService } from '../types';
import { useMessageStore, subscriptionManager, 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<void> {
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.emitConversationListAndUnreadUpdates();
}
}
store.setLoading(true);
const emitLoadingToUi = store.getConversations().length === 0;
if (emitLoadingToUi) {
subscriptionManager.notifySubscribers({
type: 'conversations_loading',
payload: { loading: true },
timestamp: Date.now(),
});
}
try {
this.remoteConversationListSource.restart();
const page = await this.remoteConversationListSource.loadNext();
// 使用 zustand set 函数正确更新状态
const newConversations = new Map<string, ConversationResponse>();
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.emitConversationListAndUnreadUpdates();
} catch (error) {
console.error('[MessageSyncService] 获取会话列表失败:', error);
if (store.getConversations().length === 0) {
const recovered = await this.hydrateConversationsFromLocalSource();
if (recovered) {
this.emitConversationListAndUnreadUpdates();
}
}
subscriptionManager.notifySubscribers({
type: 'error',
payload: { error, context: 'fetchConversations' },
timestamp: Date.now(),
});
} finally {
store.setLoading(false);
if (emitLoadingToUi) {
subscriptionManager.notifySubscribers({
type: 'conversations_loading',
payload: { loading: false },
timestamp: Date.now(),
});
}
}
}
/**
* 会话列表加载下一页
*/
async loadMoreConversations(): Promise<void> {
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.emitConversationListAndUnreadUpdates();
} catch (error) {
console.error('[MessageSyncService] 加载更多会话失败:', error);
subscriptionManager.notifySubscribers({
type: 'error',
payload: { error, context: 'loadMoreConversations' },
timestamp: Date.now(),
});
} finally {
this.loadingMoreConversations = false;
}
}
/**
* 获取单个会话详情
*/
async fetchConversationDetail(conversationId: string): Promise<ConversationResponse | null> {
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<void> {
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 getMessagesByConversation(conversationId, 20);
const localMaxSeq = await 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);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: formattedMessages,
source: 'local',
},
timestamp: Date.now(),
});
} else {
// 冷启动兜底:先下发空列表事件
store.setMessages(conversationId, []);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: [],
source: 'local_empty',
},
timestamp: Date.now(),
});
}
} 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);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: mergedSnapshot,
newMessages: snapshotMessages,
source: 'server_snapshot',
},
timestamp: Date.now(),
});
// 持久化到本地
saveMessagesBatch(snapshotMessages.map((m: any) => ({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
content: m.content,
type: m.type || 'text',
isRead: m.is_read || false,
createdAt: m.created_at,
seq: m.seq,
status: m.status || 'normal',
segments: m.segments,
}))).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);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: mergedMessages,
newMessages,
source: 'server_incremental',
},
timestamp: Date.now(),
});
saveMessagesBatch(newMessages.map((m: any) => ({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
content: m.content,
type: m.type || 'text',
isRead: m.is_read || false,
createdAt: m.created_at,
seq: m.seq,
status: m.status || 'normal',
segments: m.segments,
}))).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);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: mergedMessages,
newMessages,
source: 'server',
},
timestamp: Date.now(),
});
saveMessagesBatch(newMessages.map((m: any) => ({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
content: m.content,
type: m.type || 'text',
isRead: m.is_read || false,
createdAt: m.created_at,
seq: m.seq,
status: m.status || 'normal',
segments: m.segments,
}))).catch(error => {
console.error('[MessageSyncService] 保存消息到本地失败:', error);
});
}
}
} catch (error) {
console.error('[MessageSyncService] 获取消息失败:', error);
subscriptionManager.notifySubscribers({
type: 'error',
payload: { error, context: 'fetchMessages', conversationId },
timestamp: Date.now(),
});
} finally {
// 异步填充用户信息(不阻塞消息显示)
const currentMessages = store.getMessages(conversationId);
if (currentMessages.length > 0) {
this.userCacheService.enrichMessagesWithSenderInfo(
conversationId,
currentMessages,
(convId, enrichedMessages) => {
store.setMessages(convId, enrichedMessages);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId: convId,
messages: enrichedMessages,
source: 'sender_enriched',
},
timestamp: Date.now(),
});
}
);
}
store.setLoadingMessages(conversationId, false);
}
}
/**
* 加载更多历史消息
*/
async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise<MessageResponse[]> {
const store = useMessageStore.getState();
try {
// 先从本地获取
const localMessages = await getMessagesBeforeSeq(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);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: { conversationId, messages: mergedMessages, source: 'local_history' },
timestamp: Date.now(),
});
return formattedMessages;
}
// 本地数据不足,从服务端获取
const response = await messageService.getMessages(conversationId, undefined, beforeSeq, limit);
if (response?.messages && response.messages.length > 0) {
const serverMessages = response.messages;
await saveMessagesBatch(serverMessages.map((m: any) => ({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
content: m.content,
type: m.type || 'text',
isRead: m.is_read || false,
createdAt: m.created_at,
seq: m.seq,
status: m.status || 'normal',
segments: m.segments,
})));
const existingMessages = store.getMessages(conversationId);
const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages);
store.setMessages(conversationId, mergedMessages);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: { conversationId, messages: mergedMessages, source: 'server_history' },
timestamp: Date.now(),
});
return serverMessages;
}
return [];
} catch (error) {
console.error('[MessageSyncService] 加载更多消息失败:', error);
return [];
}
}
/**
* 获取未读数
*/
async fetchUnreadCount(): Promise<void> {
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);
// 服务端汇总未读为 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();
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
}
}
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: totalUnread,
systemUnreadCount: systemUnread,
},
timestamp: Date.now(),
});
} 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 emitConversationListAndUnreadUpdates(): void {
const store = useMessageStore.getState();
this.persistConversationListCache();
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
const unreadCount = store.getUnreadCount();
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: unreadCount.total,
systemUnreadCount: unreadCount.system,
},
timestamp: Date.now(),
});
}
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<ConversationResponse['group']> => Boolean(group));
saveConversationsWithRelatedCache(list, users, groups).catch(error => {
console.error('[MessageSyncService] 持久化会话列表失败:', error);
});
}
private async hydrateConversationsFromLocalSource(): Promise<boolean> {
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<string, ConversationResponse>();
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);
}
}

View File

@@ -0,0 +1,305 @@
/**
* 已读回执管理器
* 管理消息已读状态的同步和保护
*
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
*/
import type { ConversationResponse } from '../../../types/dto';
import { messageService } from '../../../services/messageService';
import { markConversationAsRead, updateConversationCacheUnreadCount } from '../../../services/database';
import type { ReadStateRecord, IReadReceiptManager } from '../types';
import { READ_STATE_PROTECTION_DELAY } from '../constants';
import { useMessageStore, subscriptionManager, normalizeConversationId } from '../store';
export class ReadReceiptManager implements IReadReceiptManager {
/**
* 正在进行中的已读 API 请求集合
* 用于防止 fetchConversations 的服务器数据覆盖乐观已读状态
*/
private pendingReadMap: Map<string, ReadStateRecord> = new Map();
/**
* 全局已读状态版本号,每次标记已读时递增
*/
private readStateVersion: number = 0;
/**
* 标记会话已读
* 关键方法:确保已读状态在所有组件间同步
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
const normalizedId = normalizeConversationId(conversationId);
const store = useMessageStore.getState();
const conversation = store.getConversation(normalizedId);
if (!conversation) {
console.warn('[ReadReceiptManager] 会话不存在:', normalizedId);
return;
}
const prevUnreadCount = conversation.unread_count || 0;
const existingRecord = this.pendingReadMap.get(normalizedId);
// 使用 seq 去重:同一会话若已上报到更大/相同 seq则跳过重复上报
if (existingRecord && seq <= existingRecord.lastReadSeq) {
return;
}
// 清除可能存在的旧定时器
if (existingRecord?.clearTimer) {
clearTimeout(existingRecord.clearTimer);
}
// 递增全局版本号
this.readStateVersion++;
const currentVersion = this.readStateVersion;
// 1. 标记此会话有进行中的已读请求
this.pendingReadMap.set(normalizedId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
});
// 2. 乐观更新本地状态
const updatedConv: ConversationResponse = {
...conversation,
unread_count: 0,
my_last_read_seq: Math.max(Number(conversation.my_last_read_seq || 0), seq),
};
store.updateConversation(updatedConv);
const currentUnread = store.getUnreadCount();
const newTotalUnread = Math.max(0, currentUnread.total - prevUnreadCount);
store.setUnreadCount(newTotalUnread, currentUnread.system);
// 3. 更新本地数据库
markConversationAsRead(normalizedId).catch(console.error);
// 4. 立即通知所有订阅者
subscriptionManager.notifySubscribers({
type: 'message_read',
payload: {
conversationId: normalizedId,
unreadCount: 0,
totalUnreadCount: newTotalUnread,
},
timestamp: Date.now(),
});
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: newTotalUnread,
systemUnreadCount: currentUnread.system,
},
timestamp: Date.now(),
});
// 5. 调用 API完成后设置延迟清除保护
try {
await messageService.markAsRead(normalizedId, seq);
} catch (error) {
console.error('[ReadReceiptManager] 标记已读API失败:', error);
// 失败时回滚状态
store.updateConversation(conversation);
store.setUnreadCount(currentUnread.total, currentUnread.system);
subscriptionManager.notifySubscribers({
type: 'message_read',
payload: {
conversationId: normalizedId,
unreadCount: prevUnreadCount,
totalUnreadCount: currentUnread.total,
},
timestamp: Date.now(),
});
// API 失败时立即清除保护
this.pendingReadMap.delete(normalizedId);
return;
}
// API 成功后,设置延迟清除保护
const clearTimer = setTimeout(() => {
const record = this.pendingReadMap.get(normalizedId);
if (record && record.version === currentVersion) {
this.pendingReadMap.delete(normalizedId);
}
}, READ_STATE_PROTECTION_DELAY);
// 更新记录保存定时器ID
this.pendingReadMap.set(normalizedId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
clearTimer,
});
}
/**
* 标记所有消息已读
*/
async markAllAsRead(): Promise<void> {
const store = useMessageStore.getState();
const conversations = store.conversations;
const prevTotalUnread = store.getUnreadCount().total;
const currentSystem = store.getUnreadCount().system;
// 乐观更新
conversations.forEach(conv => {
conv.unread_count = 0;
});
store.setUnreadCount(0, currentSystem);
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: 0,
systemUnreadCount: currentSystem,
},
timestamp: Date.now(),
});
try {
// 标记系统消息已读
await messageService.markAllSystemMessagesRead();
// 标记所有会话已读
const promises: Promise<any>[] = [];
conversations.forEach(conv => {
if ((conv.unread_count || 0) > 0) {
promises.push(messageService.markAsRead(conv.id, conv.last_seq));
}
});
await Promise.all(promises);
} catch (error) {
console.error('[ReadReceiptManager] 标记所有已读失败:', error);
// 回滚
store.setUnreadCount(prevTotalUnread, currentSystem);
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: prevTotalUnread,
systemUnreadCount: currentSystem,
},
timestamp: Date.now(),
});
}
}
/**
* 开始已读操作,返回版本号
*/
beginReadOperation(conversationId: string, seq: number): number {
this.readStateVersion++;
const currentVersion = this.readStateVersion;
this.pendingReadMap.set(conversationId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
});
return currentVersion;
}
/**
* 结束已读操作
*/
endReadOperation(conversationId: string, version: number, success: boolean): void {
if (!success) {
this.pendingReadMap.delete(conversationId);
return;
}
// API 成功后,设置延迟清除保护
const clearTimer = setTimeout(() => {
const record = this.pendingReadMap.get(conversationId);
if (record && record.version === version) {
this.pendingReadMap.delete(conversationId);
}
}, READ_STATE_PROTECTION_DELAY);
const existingRecord = this.pendingReadMap.get(conversationId);
if (existingRecord) {
this.pendingReadMap.set(conversationId, {
...existingRecord,
clearTimer,
});
}
}
/**
* 检查是否有进行中的已读操作
*/
isReadOperationPending(conversationId: string): boolean {
return this.pendingReadMap.has(conversationId);
}
/**
* 获取当前已读状态版本号
*/
getReadStateVersion(): number {
return this.readStateVersion;
}
/**
* 获取待处理的已读映射
*/
getPendingReadMap(): Map<string, ReadStateRecord> {
return this.pendingReadMap;
}
/**
* 根据已读保护状态智能合并会话
*/
applyConversationFromFetch(conv: ConversationResponse): ConversationResponse {
const id = normalizeConversationId(conv.id);
const readRecord = this.pendingReadMap.get(id);
let next: ConversationResponse;
if (readRecord) {
const shouldPreserveLocalRead = (conv.unread_count || 0) > 0;
next = shouldPreserveLocalRead
? { ...conv, id, unread_count: 0 }
: { ...conv, id };
} else {
next = { ...conv, id };
}
return this.normalizeConversationUnreadFromReadCursor(next);
}
/**
* 当已读游标已追上 last_seq 时,强制未读为 0
*/
normalizeConversationUnreadFromReadCursor(conv: ConversationResponse): ConversationResponse {
const lastSeq = Number(conv.last_seq || 0);
const myRead = Number(conv.my_last_read_seq ?? 0);
if (lastSeq > 0 && myRead >= lastSeq && (conv.unread_count || 0) > 0) {
return { ...conv, unread_count: 0 };
}
return conv;
}
/**
* 重置所有状态
*/
reset(): void {
// 清除所有定时器
for (const record of this.pendingReadMap.values()) {
if (record.clearTimer) {
clearTimeout(record.clearTimer);
}
}
this.pendingReadMap.clear();
this.readStateVersion = 0;
}
}

View File

@@ -0,0 +1,119 @@
/**
* 用户信息缓存服务
* 管理用户信息的缓存和获取
*/
import type { MessageResponse, UserDTO } from '../../../types/dto';
import { api } from '../../../services/api';
import { getUserCache, saveUserCache } from '../../../services/database';
import type { IUserCacheService } from '../types';
export class UserCacheService implements IUserCacheService {
// 正在获取用户信息的请求映射(用于去重)
private pendingUserRequests: Map<string, Promise<UserDTO | null>> = new Map();
/**
* 获取用户信息(带缓存和去重)
*/
async getSenderInfo(userId: string): Promise<UserDTO | null> {
// 1. 先检查本地缓存
const cachedUser = await getUserCache(userId);
if (cachedUser) {
return cachedUser;
}
// 2. 检查是否已有正在进行的请求
const pendingRequest = this.pendingUserRequests.get(userId);
if (pendingRequest) {
return pendingRequest;
}
// 3. 发起新请求
const request = this.fetchUserInfo(userId);
this.pendingUserRequests.set(userId, request);
try {
const user = await request;
return user;
} finally {
// 请求完成后清理
this.pendingUserRequests.delete(userId);
}
}
/**
* 从服务器获取用户信息
*/
private async fetchUserInfo(userId: string): Promise<UserDTO | null> {
try {
const response = await api.get<UserDTO>(`/users/${userId}`);
if (response.code === 0 && response.data) {
// 缓存到本地数据库
await saveUserCache(response.data);
return response.data;
}
return null;
} catch (error) {
console.error(`[UserCacheService] 获取用户信息失败: ${userId}`, error);
return null;
}
}
/**
* 批量异步填充消息的 sender 信息
* 填充完成后调用 notifyUpdate 更新内存并通知订阅者
*/
enrichMessagesWithSenderInfo(
conversationId: string,
messages: MessageResponse[],
notifyUpdate: (conversationId: string, messages: MessageResponse[]) => void
): void {
// 收集所有需要查询的唯一 sender_id排除系统用户
const senderIds = [...new Set(
messages
.filter(m => m.sender_id && m.sender_id !== '10000' && !m.sender)
.map(m => m.sender_id)
)];
if (senderIds.length === 0) return;
// 并发获取所有 sender 信息
Promise.all(senderIds.map(id => this.getSenderInfo(id).then(user => ({ id, user }))))
.then(results => {
const senderMap = new Map<string, UserDTO>();
results.forEach(({ id, user }) => {
if (user) senderMap.set(id, user);
});
if (senderMap.size === 0) return;
let changed = false;
const updated = messages.map(m => {
if (!m.sender && m.sender_id && senderMap.has(m.sender_id)) {
changed = true;
return { ...m, sender: senderMap.get(m.sender_id) };
}
return m;
});
if (!changed) return;
// 调用回调通知更新
notifyUpdate(conversationId, updated);
})
.catch(error => {
console.error('[UserCacheService] 批量获取 sender 信息失败:', error);
});
}
/**
* 清除所有待处理的请求
* 用于测试或需要重置的场景
*/
reset(): void {
this.pendingUserRequests.clear();
}
}
// 单例导出
export const userCacheService = new UserCacheService();

View File

@@ -0,0 +1,665 @@
/**
* WebSocket 消息处理器
* 处理所有来自 WebSocket 的消息事件
*
* 重构说明:直接使用 zustand store 替代 IMessageStateManager
*/
import type {
WSChatMessage,
WSGroupChatMessage,
WSReadMessage,
WSGroupReadMessage,
WSRecallMessage,
WSGroupRecallMessage,
WSGroupTypingMessage,
WSGroupNoticeMessage,
} from '../../../services/wsService';
import { wsService, GroupNoticeType } from '../../../services/wsService';
import { vibrateOnMessage } from '../../../services/messageVibrationService';
import { saveMessage, updateMessageStatus } from '../../../services/database';
import type { MessageResponse, ConversationResponse } from '../../../types/dto';
import type {
IWSMessageHandler,
IMessageDeduplication,
IUserCacheService,
HandleNewMessageOptions,
} from '../types';
import { MAX_FLUSH_ITERATIONS } from '../constants';
import { useMessageStore, subscriptionManager, normalizeConversationId, mergeMessagesById } from '../store';
export class WSMessageHandler implements IWSMessageHandler {
private deduplication: IMessageDeduplication;
private userCacheService: IUserCacheService;
private getCurrentUserId: () => string | null;
private markAsReadCallback: (conversationId: string, seq: number) => Promise<void>;
private fetchConversationsCallback: (forceRefresh: boolean, source: string) => Promise<void>;
private fetchUnreadCountCallback: () => Promise<void>;
private fetchMessagesCallback: (conversationId: string) => Promise<void>;
private sseUnsubscribe: (() => void) | null = null;
private isBootstrapping: boolean = false;
private lastReconnectSyncAt: number = 0;
// 初始化阶段缓冲来自 SSE 的消息事件
private bufferedChatMessages: Array<(WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean }> = [];
private bufferedReadReceipts: Array<WSReadMessage | WSGroupReadMessage> = [];
constructor(
deduplication: IMessageDeduplication,
userCacheService: IUserCacheService,
getCurrentUserId: () => string | null,
callbacks: {
markAsRead: (conversationId: string, seq: number) => Promise<void>;
fetchConversations: (forceRefresh: boolean, source: string) => Promise<void>;
fetchUnreadCount: () => Promise<void>;
fetchMessages: (conversationId: string) => Promise<void>;
}
) {
this.deduplication = deduplication;
this.userCacheService = userCacheService;
this.getCurrentUserId = getCurrentUserId;
this.markAsReadCallback = callbacks.markAsRead;
this.fetchConversationsCallback = callbacks.fetchConversations;
this.fetchUnreadCountCallback = callbacks.fetchUnreadCount;
this.fetchMessagesCallback = callbacks.fetchMessages;
}
/**
* 初始化 SSE 监听器
*/
connect(): void {
if (this.sseUnsubscribe) {
this.sseUnsubscribe();
}
const store = useMessageStore.getState();
// 监听私聊消息
wsService.on('chat', (message: WSChatMessage) => {
if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedChatMessages.push(message);
return;
}
this.handleNewMessage(message);
});
// 监听群聊消息
wsService.on('group_message', (message: WSGroupChatMessage) => {
if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedChatMessages.push(message);
return;
}
this.handleNewMessage(message);
});
// 监听私聊已读回执
wsService.on('read', (message: WSReadMessage) => {
if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedReadReceipts.push(message);
return;
}
this.handleReadReceipt(message);
});
// 监听群聊已读回执
wsService.on('group_read', (message: WSGroupReadMessage) => {
if (!store.getState().isInitialized || this.isBootstrapping) {
this.bufferedReadReceipts.push(message);
return;
}
this.handleGroupReadReceipt(message);
});
// 监听私聊消息撤回
wsService.on('recall', (message: WSRecallMessage) => {
this.handleRecallMessage(message);
});
// 监听群聊消息撤回
wsService.on('group_recall', (message: WSGroupRecallMessage) => {
this.handleGroupRecallMessage(message);
});
// 监听群聊输入状态
wsService.on('group_typing', (message: WSGroupTypingMessage) => {
this.handleGroupTyping(message);
});
// 监听群通知
wsService.on('group_notice', (message: WSGroupNoticeMessage) => {
this.handleGroupNotice(message);
});
// 监听连接状态
wsService.onConnect(() => {
store.setSSEConnected(true);
subscriptionManager.notifySubscribers({
type: 'connection_changed',
payload: { connected: true },
timestamp: Date.now(),
});
// 冷启动/重连兜底
const now = Date.now();
if (now - this.lastReconnectSyncAt > 1500) {
this.lastReconnectSyncAt = now;
const activeConversation = store.getActiveConversation();
if (!activeConversation) {
this.fetchConversationsCallback(true, 'sse-reconnect').catch(error => {
console.error('[WSMessageHandler] 连接后同步会话失败:', error);
});
} else {
this.fetchUnreadCountCallback().catch(error => {
console.error('[WSMessageHandler] 连接后同步未读数失败:', error);
});
}
if (activeConversation) {
this.fetchMessagesCallback(activeConversation).catch(error => {
console.error('[WSMessageHandler] 连接后同步当前会话消息失败:', error);
});
}
}
});
wsService.onDisconnect(() => {
store.setSSEConnected(false);
subscriptionManager.notifySubscribers({
type: 'connection_changed',
payload: { connected: false },
timestamp: Date.now(),
});
});
}
/**
* 断开 SSE 连接
*/
disconnect(): void {
if (this.sseUnsubscribe) {
this.sseUnsubscribe();
this.sseUnsubscribe = null;
}
}
/**
* 检查是否已连接
*/
isConnected(): boolean {
return useMessageStore.getState().isConnected();
}
/**
* 设置启动状态
*/
setBootstrapping(value: boolean): void {
this.isBootstrapping = value;
}
/**
* 初始化完成后,处理缓冲的 SSE 事件
*/
async flushBufferedSSEEvents(): Promise<void> {
for (let i = 0; i < MAX_FLUSH_ITERATIONS; i++) {
const hasBuffered = this.bufferedChatMessages.length > 0 || this.bufferedReadReceipts.length > 0;
if (!hasBuffered) return;
const chatEvents = this.bufferedChatMessages;
const readEvents = this.bufferedReadReceipts;
this.bufferedChatMessages = [];
this.bufferedReadReceipts = [];
// 处理消息
for (const m of chatEvents) {
await this.handleNewMessage(m, {
suppressUnreadIncrement: true,
suppressVibration: true,
suppressConversationUpdates: true,
suppressAutoMarkAsRead: true,
});
}
// 处理已读回执
for (const r of readEvents) {
if (r.type === 'read') {
this.handleReadReceipt(r);
} else {
this.handleGroupReadReceipt(r as WSGroupReadMessage);
}
}
// 使用服务器权威刷新
await this.fetchConversationsCallback(true, 'sse-buffer-flush');
await this.fetchUnreadCountCallback();
}
}
/**
* 处理新消息SSE推送
*/
async handleNewMessage(
message: (WSChatMessage | WSGroupChatMessage) & { _isAck?: boolean },
options?: HandleNewMessageOptions
): Promise<void> {
const store = useMessageStore.getState();
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
const normalizedConversationId = normalizeConversationId(conversation_id);
const currentUserId = this.getCurrentUserId();
const suppressUnreadIncrement = options?.suppressUnreadIncrement ?? false;
const suppressVibration = options?.suppressVibration ?? false;
const suppressConversationUpdates = options?.suppressConversationUpdates ?? false;
const suppressAutoMarkAsRead = options?.suppressAutoMarkAsRead ?? false;
// 消息去重检查
if (this.deduplication.isMessageProcessed(id)) {
return;
}
this.deduplication.markMessageAsProcessed(id);
// 对于群聊消息,获取发送者信息
if (message.type === 'group_message' && sender_id && sender_id !== currentUserId && sender_id !== '10000') {
this.userCacheService.getSenderInfo(sender_id).then(user => {
if (user) {
const currentMessages = store.getMessages(normalizedConversationId);
if (currentMessages) {
const updatedMessages = currentMessages.map(m =>
m.id === id ? { ...m, sender: user } : m
);
store.setMessages(normalizedConversationId, updatedMessages);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId: normalizedConversationId,
messages: [...updatedMessages],
},
timestamp: Date.now(),
});
}
}
}).catch(error => {
console.error('[WSMessageHandler] 获取发送者信息失败:', { userId: sender_id, error });
});
}
// 构造消息对象
const newMessage: MessageResponse = {
id,
conversation_id: normalizedConversationId,
sender_id,
seq,
segments: segments || [],
created_at,
status: 'normal',
};
// 立即更新内存中的消息列表
const existingMessages = store.getMessages(normalizedConversationId);
const messageExists = existingMessages.some(m => String(m.id) === String(id));
if (!messageExists) {
const updatedMessages = mergeMessagesById(existingMessages, [newMessage]);
store.setMessages(normalizedConversationId, updatedMessages);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId: normalizedConversationId,
messages: updatedMessages,
newMessage,
},
timestamp: Date.now(),
});
}
// 更新会话信息
if (!suppressConversationUpdates) {
this.updateConversationWithNewMessage(normalizedConversationId, newMessage, created_at);
}
// 更新未读数
const isCurrentUserMessage = sender_id === currentUserId;
const isActiveConversation = normalizedConversationId === store.getActiveConversation();
const shouldIncrementUnread =
!suppressUnreadIncrement &&
!isCurrentUserMessage &&
!isActiveConversation &&
!!currentUserId &&
!_isAck;
if (shouldIncrementUnread) {
if (!suppressVibration) {
const vibrationType = message.type === 'group_message' ? 'group_message' : 'chat';
vibrateOnMessage(vibrationType).catch(() => {});
}
this.incrementUnreadCount(normalizedConversationId);
}
// 如果是当前活动会话,自动标记已读
if (isActiveConversation && !isCurrentUserMessage && !suppressAutoMarkAsRead) {
this.markAsReadCallback(normalizedConversationId, seq).catch(() => {});
}
// 异步保存到本地数据库
const textContent = segments?.filter((s: any) => s.type === 'text').map((s: any) => s.data?.text || '').join('') || '';
saveMessage({
id,
conversationId: normalizedConversationId,
senderId: sender_id || '',
content: textContent,
type: segments?.find((s: any) => s.type === 'image') ? 'image' : 'text',
isRead: isActiveConversation,
createdAt: new Date(created_at).toISOString(),
seq,
status: 'normal',
segments,
}).catch(error => {
console.error('[WSMessageHandler] 保存消息到本地失败:', error);
});
// 通知收到新消息
subscriptionManager.notifySubscribers({
type: 'message_received',
payload: {
conversationId: conversation_id,
message: newMessage,
isCurrentUser: isCurrentUserMessage,
},
timestamp: Date.now(),
});
}
/**
* 处理私聊已读回执
*/
handleReadReceipt(message: WSReadMessage): void {
// 可以在这里处理对方已读的状态更新
}
/**
* 处理群聊已读回执
*/
handleGroupReadReceipt(message: WSGroupReadMessage): void {
// 群聊已读回执处理
}
/**
* 处理私聊消息撤回
*/
handleRecallMessage(message: WSRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = normalizeConversationId(conversation_id);
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
subscriptionManager.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: normalizedConversationId, messageId: message_id },
timestamp: Date.now(),
});
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[WSMessageHandler] 更新本地消息撤回状态失败:', error);
});
}
/**
* 处理群聊消息撤回
*/
handleGroupRecallMessage(message: WSGroupRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = normalizeConversationId(conversation_id);
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
subscriptionManager.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true },
timestamp: Date.now(),
});
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[WSMessageHandler] 更新本地消息撤回状态失败:', error);
});
}
/**
* 处理群聊输入状态
*/
handleGroupTyping(message: WSGroupTypingMessage): void {
const store = useMessageStore.getState();
const { group_id, user_id, is_typing } = message;
const groupIdStr = String(group_id);
const currentTypingUsers = store.getTypingUsers(groupIdStr);
let updatedTypingUsers: string[];
if (is_typing) {
if (!currentTypingUsers.includes(user_id)) {
updatedTypingUsers = [...currentTypingUsers, user_id];
} else {
updatedTypingUsers = currentTypingUsers;
}
} else {
updatedTypingUsers = currentTypingUsers.filter(id => id !== user_id);
}
if (updatedTypingUsers.length !== currentTypingUsers.length) {
store.setTypingUsers(groupIdStr, updatedTypingUsers);
subscriptionManager.notifySubscribers({
type: 'typing_status',
payload: { groupId: groupIdStr, typingUsers: updatedTypingUsers },
timestamp: Date.now(),
});
}
}
/**
* 处理群通知
*/
handleGroupNotice(message: WSGroupNoticeMessage): void {
const store = useMessageStore.getState();
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
const groupIdStr = String(group_id);
// 处理禁言/解除禁言通知
if (notice_type === 'muted' || notice_type === 'unmuted') {
const mutedUserId = data?.user_id;
const currentUserId = this.getCurrentUserId();
if (mutedUserId && mutedUserId === currentUserId) {
const isMuted = notice_type === 'muted';
store.setMutedStatus(groupIdStr, isMuted);
}
}
// 补一条系统消息到当前会话消息流
if (message_id && typeof seq === 'number' && seq > 0) {
const conversationId = this.findConversationIdByGroupId(groupIdStr);
if (conversationId) {
const existingMessages = store.getMessages(conversationId);
const exists = existingMessages.some(m => String(m.id) === String(message_id));
if (!exists) {
const noticeText = this.buildGroupNoticeText(notice_type, data);
const systemNoticeMessage: MessageResponse = {
id: String(message_id),
conversation_id: conversationId,
sender_id: '10000',
seq,
segments: noticeText
? [{ type: 'text', data: { text: noticeText } as any }]
: [],
status: 'normal',
category: 'notification',
created_at: new Date(timestamp || Date.now()).toISOString(),
};
const updatedMessages = [...existingMessages, systemNoticeMessage].sort((a, b) => a.seq - b.seq);
store.setMessages(conversationId, updatedMessages);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: {
conversationId,
messages: updatedMessages,
newMessage: systemNoticeMessage,
source: 'group_notice',
},
timestamp: Date.now(),
});
}
}
}
// 通知订阅者群通知
subscriptionManager.notifySubscribers({
type: 'group_notice',
payload: {
groupId: groupIdStr,
noticeType: notice_type,
data,
timestamp,
messageId: message_id,
seq,
},
timestamp: Date.now(),
});
}
// ==================== 私有工具方法 ====================
private updateConversationWithNewMessage(
conversationId: string,
message: MessageResponse,
createdAt: string
): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (conversation) {
const updatedConv: ConversationResponse = {
...conversation,
last_seq: message.seq,
last_message: message,
last_message_at: createdAt,
updated_at: createdAt,
};
store.updateConversation(updatedConv);
}
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
}
private incrementUnreadCount(conversationId: string): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (conversation) {
const prevUnreadCount = conversation.unread_count || 0;
const updatedConv: ConversationResponse = {
...conversation,
unread_count: prevUnreadCount + 1,
};
store.updateConversation(updatedConv);
const currentUnread = store.getUnreadCount();
store.setUnreadCount(currentUnread.total + 1, currentUnread.system);
subscriptionManager.notifySubscribers({
type: 'unread_count_updated',
payload: {
totalUnreadCount: store.getUnreadCount().total,
systemUnreadCount: store.getUnreadCount().system,
},
timestamp: Date.now(),
});
subscriptionManager.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: store.getConversations() },
timestamp: Date.now(),
});
}
}
private markMessageAsRecalled(conversationId: string, messageId: string): void {
const store = useMessageStore.getState();
const messages = store.getMessages(conversationId);
if (!messages) return;
let changed = false;
const updatedMessages: MessageResponse[] = messages.map((m): MessageResponse => {
if (String(m.id) !== String(messageId) || m.status === 'recalled') {
return m;
}
changed = true;
return {
...m,
status: 'recalled' as MessageResponse['status'],
segments: [],
};
});
if (!changed) return;
store.setMessages(conversationId, updatedMessages);
subscriptionManager.notifySubscribers({
type: 'messages_updated',
payload: { conversationId, messages: updatedMessages },
timestamp: Date.now(),
});
}
private syncConversationLastMessageOnRecall(conversationId: string, messageId: string): void {
const store = useMessageStore.getState();
const conversation = store.getConversation(conversationId);
if (!conversation?.last_message) return;
if (String(conversation.last_message.id) !== String(messageId)) return;
if (conversation.last_message.status === 'recalled') return;
const updatedConversation: ConversationResponse = {
...conversation,
last_message: {
...conversation.last_message,
status: 'recalled',
},
};
store.updateConversation(updatedConversation);
}
private findConversationIdByGroupId(groupId: string): string | null {
const store = useMessageStore.getState();
const conversations = store.getConversations();
for (const conv of conversations) {
if (String(conv.group?.id || '') === groupId) {
return conv.id;
}
}
return null;
}
private buildGroupNoticeText(noticeType: GroupNoticeType, data: any): string {
const username = data?.username || '用户';
switch (noticeType) {
case 'member_join':
return `"${username}" 加入了群聊`;
case 'member_leave':
return `"${username}" 退出了群聊`;
case 'member_removed':
return `"${username}" 被移出群聊`;
case 'muted':
return `"${username}" 已被管理员禁言`;
case 'unmuted':
return `"${username}" 已被管理员解除禁言`;
default:
return '';
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* 消息服务层统一导出
*
* 重构说明:所有服务直接使用 zustand store移除了 IMessageStateManager 接口依赖
*/
// 消息去重服务
export { MessageDeduplication, messageDeduplication } from './MessageDeduplication';
// 用户缓存服务
export { UserCacheService, userCacheService } from './UserCacheService';
// 已读回执管理器
export { ReadReceiptManager } from './ReadReceiptManager';
// 会话操作服务
export { ConversationOperations } from './ConversationOperations';
// 消息发送服务
export { MessageSendService } from './MessageSendService';
// 消息同步服务
export { MessageSyncService } from './MessageSyncService';
// WebSocket 消息处理器
export { WSMessageHandler } from './WSMessageHandler';