Files
frontend/src/services/messageService.ts

708 lines
20 KiB
TypeScript
Raw Normal View History

/**
*
* /
* 使 API : /api/v1/conversations (RESTful action )
*/
import { api } from './api';
import {
ConversationListResponse,
ConversationResponse,
ConversationDetailResponse,
MessageListResponse,
MessageResponse,
SendMessageRequest,
UnreadCountResponse,
ConversationUnreadCountResponse,
SystemMessageListResponse,
SystemMessageResponse,
SystemUnreadCountResponse,
MessageSegment,
CursorPaginationRequest,
CursorPaginationResponse,
} from '../types/dto';
import {
getConversationCache,
getConversationListCache,
saveConversationCache,
saveConversationsCache,
saveGroupsCache,
saveUsersCache,
updateConversationCacheUnreadCount,
} from './database';
// 消息服务类
class MessageService {
private async refreshConversationsFromServer(
page = 1,
pageSize = 20
): Promise<ConversationListResponse> {
const response = await api.get<ConversationListResponse>('/conversations', {
page,
page_size: pageSize,
});
const list = response.data.list || [];
await saveConversationsCache(list);
await saveUsersCache(
list.flatMap(conv => [
...(conv.participants || []),
...(conv.last_message?.sender ? [conv.last_message.sender] : []),
])
);
await saveGroupsCache(
list
.map(conv => conv.group)
.filter((group): group is NonNullable<typeof group> => Boolean(group))
);
return response.data;
}
private async refreshConversationDetailFromServer(id: string): Promise<ConversationDetailResponse> {
const response = await api.get<ConversationDetailResponse>(
`/conversations/${encodeURIComponent(id)}`
);
await saveConversationCache(response.data);
if (response.data.participants?.length) {
await saveUsersCache(response.data.participants);
}
if (response.data.last_message?.sender) {
await saveUsersCache([response.data.last_message.sender]);
}
return response.data;
}
private normalizeCachedDetail(
id: string,
cached: any
): ConversationDetailResponse {
return {
id: String(cached?.id || id),
type: cached?.type || 'private',
is_pinned: Boolean(cached?.is_pinned),
last_seq: Number(cached?.last_seq || 0),
last_message: cached?.last_message,
last_message_at: cached?.last_message_at || cached?.updated_at || new Date().toISOString(),
unread_count: Number(cached?.unread_count || 0),
participants: Array.isArray(cached?.participants) ? cached.participants : [],
my_last_read_seq: Number(cached?.my_last_read_seq || 0),
other_last_read_seq: Number(cached?.other_last_read_seq || 0),
created_at: cached?.created_at || new Date().toISOString(),
updated_at: cached?.updated_at || new Date().toISOString(),
};
}
// ==================== 会话相关 ====================
/**
*
* GET /api/v1/conversations/list
*/
async getConversations(
page = 1,
pageSize = 20,
forceRefresh = false
): Promise<ConversationListResponse> {
// 如果强制刷新,直接从服务器获取,不使用缓存
if (!forceRefresh) {
const cachedList = await getConversationListCache();
if (cachedList.length > 0) {
// 后台刷新数据,但不阻塞当前返回
this.refreshConversationsFromServer(page, pageSize).catch(error => {
console.error('后台刷新会话列表失败:', error);
});
return {
list: cachedList,
total: cachedList.length,
page: 1,
page_size: cachedList.length,
total_pages: 1,
};
}
}
// 强制刷新或没有缓存时,直接从服务器获取
try {
return await this.refreshConversationsFromServer(page, pageSize);
} catch (error) {
console.error('获取会话列表失败:', error);
return {
list: [],
total: 0,
page: 1,
page_size: pageSize,
total_pages: 0,
};
}
}
/**
*
* POST /api/v1/conversations
* @param userId ID (UUID格式)
*/
async createConversation(userId: string): Promise<ConversationResponse> {
const response = await api.post<ConversationResponse>('/conversations', {
user_id: userId,
});
await saveConversationCache(response.data);
if (response.data.participants?.length) {
await saveUsersCache(response.data.participants);
}
return response.data;
}
/**
*
* GET /api/v1/conversations/:id
*/
async getConversationById(id: string): Promise<ConversationDetailResponse> {
const cached = await getConversationCache(id);
if (cached) {
this.refreshConversationDetailFromServer(id).catch(error => {
console.error('后台刷新会话详情失败:', error);
});
return this.normalizeCachedDetail(id, cached);
}
try {
return await this.refreshConversationDetailFromServer(id);
} catch (error) {
console.error('获取会话详情失败:', error);
throw error;
}
}
/**
*
* DELETE /api/v1/conversations/:id/self
*/
async deleteConversationForSelf(conversationId: string): Promise<void> {
await api.delete(`/conversations/${encodeURIComponent(conversationId)}/self`);
}
/**
*
* PUT /api/v1/conversations/:id/pinned
*/
async setConversationPinned(conversationId: string, isPinned: boolean): Promise<void> {
await api.put(`/conversations/${encodeURIComponent(conversationId)}/pinned`, {
is_pinned: isPinned,
});
}
// ==================== 消息相关 ====================
/**
*
* GET /api/v1/conversations/:id/messages
* @param conversationId ID
* @param afterSeq
* @param beforeSeq
* @param limit
*/
async getMessages(
conversationId: string,
afterSeq?: number,
beforeSeq?: number,
limit?: number
): Promise<MessageListResponse> {
const params: Record<string, any> = {};
if (afterSeq !== undefined) {
params.after_seq = afterSeq;
}
if (beforeSeq !== undefined) {
params.before_seq = beforeSeq;
}
if (limit !== undefined) {
params.limit = limit;
}
try {
const response = await api.get<any>(
`/conversations/${encodeURIComponent(conversationId)}/messages`,
params
);
// 处理增量同步响应after_seq 或 before_seq
if ((afterSeq !== undefined || beforeSeq !== undefined) && response.data.messages !== undefined) {
return {
messages: response.data.messages,
total: response.data.messages.length,
page: 1,
page_size: limit || 100,
};
}
// 处理分页响应(后端返回 list 字段)
const data = response.data;
return {
messages: data.list || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 20,
};
} catch (error) {
console.error('获取消息列表失败:', error);
return {
messages: [],
total: 0,
page: 1,
page_size: 20,
};
}
}
/**
*
* POST /api/v1/conversations/:id/messages
* Body: { detail_type, segments }
*/
async sendMessage(
conversationId: string,
data: SendMessageRequest,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
// 直接使用传入的 segments
if (!data.segments || data.segments.length === 0) {
throw new Error('消息内容不能为空');
}
return this.sendMessageByAction(
detailType,
conversationId,
data.segments,
data.reply_to_id
);
}
/**
* HTTP POST
* 请求格式: POST /api/v1/conversations/:id/messages
* Body: { "detail_type": "private", "segments": [...] }
* @param detailType : 'private' , 'group'
* @param conversationId ID
* @param message [{"type": "text", "data": {"text": "xxx"}}]
* @param replyToId ID
* @returns
*/
async sendMessageByAction(
detailType: 'private' | 'group',
conversationId: string,
message: MessageSegment[],
replyToId?: string
): Promise<MessageResponse> {
try {
const body: Record<string, any> = {
detail_type: detailType,
segments: message,
};
if (replyToId) {
body.reply_to_id = replyToId;
}
const response = await api.post<MessageResponse>(
`/conversations/${encodeURIComponent(conversationId)}/messages`,
body
);
return response.data;
} catch (error) {
console.error('发送消息失败:', error);
throw error;
}
}
/**
* 便
*/
async sendTextMessage(
conversationId: string,
content: string,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
return this.sendTextMessageByAction(detailType, conversationId, content);
}
/**
* HTTP POST
* @param detailType : 'private' , 'group'
* @param conversationId ID
* @param text
* @returns
*/
async sendTextMessageByAction(
detailType: 'private' | 'group',
conversationId: string,
text: string
): Promise<MessageResponse> {
const message: MessageSegment[] = [
{
type: 'text',
data: {
text: text,
},
},
];
return this.sendMessageByAction(detailType, conversationId, message);
}
/**
* 便
*/
async sendImageMessage(
conversationId: string,
mediaUrl: string,
content?: string,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
const message: MessageSegment[] = [
{
type: 'image',
data: {
url: mediaUrl,
},
},
];
// 如果有文本说明,添加一个文本段
if (content) {
message.unshift({
type: 'text',
data: { text: content },
});
}
return this.sendMessageByAction(detailType, conversationId, message);
}
/**
* segments
* @param conversationId ID
* @param segments
* @param detailType
*/
async sendSegmentsMessage(
conversationId: string,
segments: MessageSegment[],
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
return this.sendMessageByAction(detailType, conversationId, segments);
}
/**
* segments
* @param conversationId ID
* @param segments
* @param replyToId ID
* @param detailType
*/
async sendReplySegmentsMessage(
conversationId: string,
segments: MessageSegment[],
replyToId: string,
detailType: 'private' | 'group' = 'private'
): Promise<MessageResponse> {
// 使用 reply_to_id 参数发送回复消息
return this.sendMessageByAction(detailType, conversationId, segments, replyToId);
}
/**
* /
* POST /api/v1/messages/delete_msg
* Body: { "message_id": "xxx" }
*/
async recallMessage(messageId: string): Promise<void> {
await api.post('/messages/delete_msg', {
message_id: messageId,
});
}
/**
*
* POST /api/v1/messages/delete_msg
*/
async deleteMessage(messageId: string): Promise<void> {
await this.recallMessage(messageId);
}
// ==================== 已读相关 ====================
/**
*
* POST /api/v1/conversations/:id/read
* @param conversationId ID
* @param seq
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
await api.post(`/conversations/${encodeURIComponent(conversationId)}/read`, {
last_read_seq: Number(seq),
});
// 立即清零本地缓存中的 unread_count避免回到列表时仍显示旧红点
updateConversationCacheUnreadCount(conversationId, 0).catch(error => {
console.error('更新本地会话未读数失败:', error);
});
}
/**
*
* POST /api/v1/conversations/:id/typing
*/
async sendTyping(conversationId: string): Promise<void> {
await api.post(`/conversations/${encodeURIComponent(conversationId)}/typing`);
}
/**
*
* GET /api/v1/conversations/unread/count
*/
async getUnreadCount(): Promise<UnreadCountResponse> {
try {
const response = await api.get<UnreadCountResponse>(
'/conversations/unread/count'
);
return response.data;
} catch (error) {
console.error('获取未读总数失败:', error);
return { total_unread_count: 0 };
}
}
/**
*
* GET /api/v1/conversations/unread/count?conversation_id=xxx
*/
async getConversationUnreadCount(
conversationId: string
): Promise<ConversationUnreadCountResponse> {
try {
const response = await api.get<ConversationUnreadCountResponse>(
'/conversations/unread/count',
{ conversation_id: conversationId }
);
return response.data;
} catch (error) {
console.error('获取会话未读数失败:', error);
return {
conversation_id: conversationId,
unread_count: 0,
};
}
}
// ==================== 系统消息相关 ====================
/**
*
* GET /api/v1/messages/system
* @param limit
* @param beforeId ID之前的消息
*/
async getSystemMessages(
pageSize: number = 20,
page: number = 1
): Promise<SystemMessageListResponse> {
try {
const response = await api.get<{
list: any[];
total: number;
page: number;
page_size: number;
total_pages: number;
}>('/messages/system', {
page,
page_size: pageSize,
});
const data = response.data;
return {
messages: data.list || [],
total: data.total || 0,
has_more: data.page < data.total_pages,
};
} catch (error) {
console.error('获取系统消息列表失败:', error);
return {
messages: [],
total: 0,
has_more: false,
};
}
}
/**
*
* GET /api/v1/messages/system/unread-count
*/
async getSystemUnreadCount(): Promise<SystemUnreadCountResponse> {
try {
const response = await api.get<SystemUnreadCountResponse>(
'/messages/system/unread-count'
);
return response.data;
} catch (error) {
console.error('获取系统消息未读数失败:', error);
return { unread_count: 0 };
}
}
/**
*
* PUT /api/v1/messages/system/:messageId/read
* @param messageId ID
*/
async markSystemMessageRead(messageId: string): Promise<void> {
await api.put(`/messages/system/${encodeURIComponent(messageId)}/read`);
}
/**
*
* PUT /api/v1/messages/system/read-all
*/
async markAllSystemMessagesRead(): Promise<void> {
await api.put('/messages/system/read-all');
}
// ==================== 游标分页方法 ====================
/**
*
* GET /api/v1/conversations/cursor
* @param params
*/
async getConversationsCursor(
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<ConversationResponse>> {
try {
const response = await api.get<CursorPaginationResponse<ConversationResponse>>(
'/conversations/cursor',
params
);
const raw = response.data;
return {
list: Array.isArray(raw?.list) ? raw.list : [],
next_cursor: raw?.next_cursor ?? null,
prev_cursor: raw?.prev_cursor ?? null,
has_more: raw?.has_more ?? false,
};
} catch (error) {
console.error('获取会话列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
/**
*
* GET /api/v1/conversations/:id/messages/cursor
* @param conversationId ID
* @param params
*/
async getMessagesCursor(
conversationId: string,
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<MessageResponse>> {
try {
const response = await api.get<any>(
`/conversations/${encodeURIComponent(conversationId)}/messages/cursor`,
params
);
const raw = response.data;
return {
list: Array.isArray(raw?.list) ? raw.list : [],
next_cursor: raw?.next_cursor ?? null,
prev_cursor: raw?.prev_cursor ?? null,
has_more: raw?.has_more ?? false,
};
} catch (error) {
console.error('获取消息列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
/**
*
* GET /api/v1/messages/system/cursor
* @param params
*/
async getSystemMessagesCursor(
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<SystemMessageResponse>> {
try {
// 始终传递 cursor 参数(空字符串表示首次请求),确保使用游标分页
const requestParams: Record<string, any> = {
cursor: params.cursor || '',
page_size: params.page_size,
};
const response = await api.get<any>('/messages/system', requestParams);
const data = response.data;
// 游标或带 page/total_pages 的分页(统一为 CursorPaginationResponselist
if (data && typeof data === 'object') {
const list = Array.isArray(data.list) ? data.list : null;
if (list) {
const isPageShape =
typeof data.page === 'number' &&
typeof data.total_pages === 'number' &&
data.next_cursor === undefined &&
data.prev_cursor === undefined;
if (isPageShape) {
const currentPage = data.page || 1;
const totalPages = data.total_pages || 1;
return {
list,
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
has_more: currentPage < totalPages,
};
}
return {
list,
next_cursor: data.next_cursor ?? null,
prev_cursor: data.prev_cursor ?? null,
has_more: data.has_more ?? false,
};
}
}
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
} catch (error) {
console.error('获取系统消息列表失败:', error);
return {
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,
};
}
}
}
// 导出消息服务实例
export const messageService = new MessageService();
// 导出类型以方便使用
export type {
ConversationListResponse,
ConversationResponse,
ConversationDetailResponse,
MessageListResponse,
MessageResponse,
SendMessageRequest,
UnreadCountResponse,
ConversationUnreadCountResponse,
SystemMessageListResponse,
SystemUnreadCountResponse,
};