Migrate frontend realtime messaging to SSE.

Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts.

Made-with: Cursor
This commit is contained in:
2026-03-10 12:58:23 +08:00
parent 63e32b15a3
commit be84c01abd
25 changed files with 974 additions and 1305 deletions

View File

@@ -16,7 +16,7 @@
import { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../types/dto';
import { messageService } from '../services/messageService';
import {
websocketService,
sseService,
WSChatMessage,
WSGroupChatMessage,
WSReadMessage,
@@ -26,7 +26,7 @@ import {
WSGroupTypingMessage,
WSGroupNoticeMessage,
GroupNoticeType,
} from '../services/websocketService';
} from '../services/sseService';
import {
saveMessage,
saveMessagesBatch,
@@ -39,7 +39,7 @@ import {
CachedMessage,
getUserCache,
saveUserCache,
deleteMessage as deleteMessageFromDb,
updateMessageStatus,
deleteConversation as deleteConversationFromDb,
} from '../services/database';
import { api } from '../services/api';
@@ -326,47 +326,47 @@ class MessageManager {
// 监听私聊消息
websocketService.on('chat', (message: WSChatMessage) => {
sseService.on('chat', (message: WSChatMessage) => {
this.handleNewMessage(message);
});
// 监听群聊消息
websocketService.on('group_message', (message: WSGroupChatMessage) => {
sseService.on('group_message', (message: WSGroupChatMessage) => {
this.handleNewMessage(message);
});
// 监听私聊已读回执
websocketService.on('read', (message: WSReadMessage) => {
sseService.on('read', (message: WSReadMessage) => {
this.handleReadReceipt(message);
});
// 监听群聊已读回执
websocketService.on('group_read', (message: WSGroupReadMessage) => {
sseService.on('group_read', (message: WSGroupReadMessage) => {
this.handleGroupReadReceipt(message);
});
// 监听私聊消息撤回
websocketService.on('recall', (message: WSRecallMessage) => {
sseService.on('recall', (message: WSRecallMessage) => {
this.handleRecallMessage(message);
});
// 监听群聊消息撤回
websocketService.on('group_recall', (message: WSGroupRecallMessage) => {
sseService.on('group_recall', (message: WSGroupRecallMessage) => {
this.handleGroupRecallMessage(message);
});
// 监听群聊输入状态
websocketService.on('group_typing', (message: WSGroupTypingMessage) => {
sseService.on('group_typing', (message: WSGroupTypingMessage) => {
this.handleGroupTyping(message);
});
// 监听群通知
websocketService.on('group_notice', (message: WSGroupNoticeMessage) => {
sseService.on('group_notice', (message: WSGroupNoticeMessage) => {
this.handleGroupNotice(message);
});
// 监听连接状态
websocketService.onConnect(() => {
sseService.onConnect(() => {
this.state.isWebSocketConnected = true;
this.notifySubscribers({
type: 'connection_changed',
@@ -389,7 +389,7 @@ class MessageManager {
}
});
websocketService.onDisconnect(() => {
sseService.onDisconnect(() => {
this.state.isWebSocketConnected = false;
this.notifySubscribers({
type: 'connection_changed',
@@ -761,36 +761,93 @@ class MessageManager {
private handleGroupReadReceipt(message: WSGroupReadMessage): void {
}
/**
* 将指定消息标记为已撤回(保留占位,不删除)
*/
private markMessageAsRecalled(conversationId: string, messageId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const messages = this.state.messagesMap.get(normalizedConversationId);
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;
}
this.state.messagesMap.set(normalizedConversationId, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: normalizedConversationId, messages: updatedMessages },
timestamp: Date.now(),
});
}
/**
* 如果撤回的是会话最后一条消息,同步会话列表中的 last_message 状态
*/
private syncConversationLastMessageOnRecall(conversationId: string, messageId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const conversation = this.state.conversations.get(normalizedConversationId);
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',
},
};
this.state.conversations.set(normalizedConversationId, updatedConversation);
this.updateConversationList();
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
/**
* 处理私聊消息撤回
*/
private handleRecallMessage(message: WSRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
// 从消息列表中移除被撤回的消息
const messages = this.state.messagesMap.get(conversation_id);
if (messages) {
const updatedMessages = messages.filter(m => m.id !== message_id);
if (updatedMessages.length !== messages.length) {
this.state.messagesMap.set(conversation_id, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: conversation_id, messages: updatedMessages },
timestamp: Date.now(),
});
}
}
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
// 通知订阅者消息被撤回
this.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: conversation_id, messageId: message_id },
payload: { conversationId: normalizedConversationId, messageId: message_id },
timestamp: Date.now(),
});
// 本地数据库删除
deleteMessageFromDb(message_id).catch(error => {
console.error('[MessageManager] 删除本地消息失败:', error);
// 同步本地数据库状态,避免冷启动后撤回占位丢失
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[MessageManager] 更新本地消息撤回状态失败:', error);
});
}
@@ -799,31 +856,21 @@ class MessageManager {
*/
private handleGroupRecallMessage(message: WSGroupRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
// 从消息列表中移除被撤回的消息
const messages = this.state.messagesMap.get(conversation_id);
if (messages) {
const updatedMessages = messages.filter(m => m.id !== message_id);
if (updatedMessages.length !== messages.length) {
this.state.messagesMap.set(conversation_id, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: conversation_id, messages: updatedMessages },
timestamp: Date.now(),
});
}
}
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
// 通知订阅者消息被撤回
this.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: conversation_id, messageId: message_id, isGroup: true },
payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true },
timestamp: Date.now(),
});
// 本地数据库删除
deleteMessageFromDb(message_id).catch(error => {
console.error('[MessageManager] 删除本地消息失败:', error);
// 同步本地数据库状态,避免冷启动后撤回占位丢失
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[MessageManager] 更新本地消息撤回状态失败:', error);
});
}