import webSocket from '@ohos.net.webSocket' import http from '@ohos.net.http' import { HttpClient } from '../core/HttpClient' import { SDKContext } from '../core/SDKContext' import { DEFAULT_API_BASE_URL, DEFAULT_IM_WS_URL } from '../core/Endpoints' import type { ChatType, ConversationData, FriendRequest, GroupJoinRequest, HistoryQuery, ImGroup, ImMessage, MsgType, PageResult, SendMessageParams, UserProfile, } from '../core/Types' export interface ImEventDelegate { onConnected?(): void onDisconnected?(code: number, reason: string): void onMessage?(msg: ImMessage): void onRead?(msg: ImMessage): void onRevoke?(data: RevokeData): void onError?(message: string): void onConversationsChange?(conversations: ConversationData[]): void } export interface RevokeData { msgId: string operatorId: string } class WebSocketFrame { type: string = '' payload: Object = new Object() } class WebSocketEnvelope { destination: string = '' payload: Object = new Object() } class AppBody { appId: string = '' } class HistoryQueryParams { appId: string = '' page: number = 0 size: number = 0 msgType: MsgType = 'TEXT' keyword: string = '' startTime: string = '' endTime: string = '' } class ConversationActionBody { appId: string = '' chatType: ChatType = 'SINGLE' } class FriendRequestQueryBody { appId: string = '' direction: 'incoming' | 'outgoing' = 'incoming' } class FriendRequestBody { appId: string = '' toUserId: string = '' remark: string = '' } class GroupJoinRequestBody { appId: string = '' remark: string = '' } class EditMessageBody { content: string = '' } class UpdateProfileBody { appId: string = '' nickname: string = '' avatar: string = '' gender: string = '' } class DraftBody { appId: string = '' chatType: ChatType = 'SINGLE' draft: string = '' } class PinBody { appId: string = '' chatType: ChatType = 'SINGLE' pinned: boolean = false } class MuteBody { appId: string = '' chatType: ChatType = 'SINGLE' muted: boolean = false } class SendEnvelopePayload { messageId: string = '' toId: string = '' chatType: ChatType = 'SINGLE' msgType: MsgType = 'TEXT' content: string = '' mentionedUserIds: string = '' } class RevokeEnvelopePayload { msgId: string = '' } const MAX_RECONNECT_DELAY = 30_000 export class ImClient { private ws: webSocket.WebSocket | null = null private reconnectDelay: number = 3_000 private reconnectTimer: number | null = null private destroyed: boolean = false delegate: ImEventDelegate | null = null connect(): void { if (this.destroyed) return const config = SDKContext.getConfig() const token = SDKContext.getToken() ?? '' const url = DEFAULT_IM_WS_URL.replace(/\/$/, '') + '?token=' + encodeURIComponent(token) this.ws = webSocket.createWebSocket() this.ws.on('open', (_err: Error, _value: Object) => { this.reconnectDelay = 3_000 if (config.debug) console.log('[ImClient] connected') this.delegate?.onConnected?.() }) this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => { try { if (typeof value !== 'string') { return } const frame = JSON.parse(value) as WebSocketFrame if (frame.type === 'MESSAGE') { const message = this.normalizeMessage(frame.payload as ImMessage) if (message.status === 'READ') { this.delegate?.onRead?.(message) } if (message.revoked || message.status === 'REVOKED' || message.msgType === 'REVOKED') { this.delegate?.onRevoke?.({ msgId: message.id, operatorId: message.fromId }) } this.delegate?.onMessage?.(message) this.notifyConversations() } else if (frame.type === 'REVOKE') { this.delegate?.onRevoke?.(frame.payload as RevokeData) this.notifyConversations() } } catch { // ignore malformed frames } }) this.ws.on('close', (_err: Error, value: webSocket.CloseResult) => { this.delegate?.onDisconnected?.(value.code, value.reason) if (!this.destroyed) this.scheduleReconnect() }) this.ws.on('error', (_err: Error) => { this.delegate?.onError?.(_err.message) }) this.ws.connect(url, {}) } send(params: SendMessageParams): ImMessage { const outgoing = this.buildOutgoingMessage(params) if (!this.ws) { return this.markFailed(outgoing) } const payload = new SendEnvelopePayload() payload.messageId = outgoing.id payload.toId = params.toId payload.chatType = params.chatType payload.msgType = params.msgType payload.content = params.content if (params.mentionedUserIds !== undefined) { payload.mentionedUserIds = params.mentionedUserIds } const envelope = new WebSocketEnvelope() envelope.destination = '/app/chat.send' envelope.payload = payload this.ws.send(JSON.stringify(envelope), (_err: Error) => { if (_err) { this.delegate?.onError?.(_err.message) } }) return outgoing } revoke(msgId: string): void { if (!this.ws) { throw new Error('WebSocket not connected') } const payload = new RevokeEnvelopePayload() payload.msgId = msgId const envelope = new WebSocketEnvelope() envelope.destination = '/app/chat.revoke' envelope.payload = payload this.ws.send(JSON.stringify(envelope), (_err: Error) => { if (_err) this.delegate?.onError?.(_err.message) }) } async fetchHistory( toId: string, page: number = 0, size: number = 20, query: HistoryQuery = {}, ): Promise> { const queryString = this.buildHistoryQuery(page, size, query) return HttpClient.get>('/api/im/messages/history/' + encodeURIComponent(toId), queryString) } async fetchGroupHistory( groupId: string, page: number = 0, size: number = 50, query: HistoryQuery = {}, ): Promise> { const queryString = this.buildHistoryQuery(page, size, query) return HttpClient.get>('/api/im/messages/group-history/' + encodeURIComponent(groupId), queryString) } async locateHistoryPage( toId: string, messageId: string, pageSize: number = 20, maxPages: number = 20, ): Promise { const pageCount = Math.max(maxPages, 1) for (let page = 0; page < pageCount; page += 1) { const result = await this.fetchHistory(toId, page, pageSize) const messages = result.content ?? [] if (messages.some(item => item.id === messageId)) { return messages } if (messages.length < pageSize) { return null } } return null } async locateGroupHistoryPage( groupId: string, messageId: string, pageSize: number = 50, maxPages: number = 20, ): Promise { const pageCount = Math.max(maxPages, 1) for (let page = 0; page < pageCount; page += 1) { const result = await this.fetchGroupHistory(groupId, page, pageSize) const messages = result.content ?? [] if (messages.some(item => item.id === messageId)) { return messages } if (messages.length < pageSize) { return null } } return null } async listConversations(size: number = 20): Promise { return HttpClient.get('/api/im/conversations', this.buildConversationQuery(size)) } async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise { await HttpClient.put('/api/im/conversations/' + encodeURIComponent(targetId) + '/read', undefined, this.buildConversationActionQuery(chatType)) } async setDraft(targetId: string, chatType: ChatType, draft: string): Promise { const params = new DraftBody() params.appId = SDKContext.getConfig().appKey params.chatType = chatType params.draft = draft await HttpClient.put('/api/im/conversations/' + encodeURIComponent(targetId) + '/draft', params) } async setConversationPinned(targetId: string, chatType: ChatType, pinned: boolean): Promise { const params = new PinBody() params.appId = SDKContext.getConfig().appKey params.chatType = chatType params.pinned = pinned await HttpClient.put('/api/im/conversations/' + encodeURIComponent(targetId) + '/pinned', params) } async setConversationMuted(targetId: string, chatType: ChatType, muted: boolean): Promise { const params = new MuteBody() params.appId = SDKContext.getConfig().appKey params.chatType = chatType params.muted = muted await HttpClient.put('/api/im/conversations/' + encodeURIComponent(targetId) + '/muted', params) } async deleteConversation(targetId: string, chatType: ChatType): Promise { await HttpClient.delete('/api/im/conversations/' + encodeURIComponent(targetId), this.buildConversationActionQuery(chatType)) } async listFriends(): Promise { return HttpClient.get('/api/im/friends', this.buildAppQuery()) } async listGroups(): Promise { return HttpClient.get('/api/im/groups', this.buildAppQuery()) } async getGroupInfo(groupId: string): Promise { return HttpClient.get('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery()) } async listGroupMembers(groupId: string): Promise { return HttpClient.get( '/api/im/groups/' + encodeURIComponent(groupId) + '/members', this.buildAppQuery(), ) } async searchGroupMembers(groupId: string, keyword: string, size: number = 20): Promise { return HttpClient.get( '/api/im/groups/' + encodeURIComponent(groupId) + '/members/search', this.buildSearchQuery(keyword, size), ) } async searchUsers(keyword: string, size: number = 20): Promise { return HttpClient.get( '/api/im/admin/users/search', this.buildSearchQuery(keyword, size), ) } async searchGroups(keyword: string, size: number = 20): Promise { return HttpClient.get( '/api/im/admin/groups/search', this.buildSearchQuery(keyword, size), ) } async searchMessages( keyword: string = '', chatType: ChatType | '' = '', msgType: MsgType | '' = '', page: number = 0, size: number = 20, ): Promise> { return HttpClient.get>( '/api/im/admin/messages/search', this.buildMessageSearchQuery(keyword, chatType, msgType, page, size), ) } async editMessage(messageId: string, content: string): Promise { const body = new EditMessageBody() body.content = content return HttpClient.put( '/api/im/messages/' + encodeURIComponent(messageId), body, this.buildAppQuery(), ) } async revokeMessage(messageId: string): Promise { return HttpClient.post( '/api/im/messages/' + encodeURIComponent(messageId) + '/revoke', this.buildAppBody(), this.buildAppQuery(), ) } async sendFriendRequest(toUserId: string, remark: string | null = null): Promise { const params = new FriendRequestBody() params.appId = SDKContext.getConfig().appKey params.toUserId = toUserId if (remark !== null && remark !== '') { params.remark = remark } return HttpClient.post('/api/im/friend-requests', params) } async listFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming'): Promise { return HttpClient.get('/api/im/friend-requests', this.buildFriendRequestQuery(direction)) } async acceptFriendRequest(requestId: string): Promise { return HttpClient.post('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/accept', this.buildAppBody()) } async rejectFriendRequest(requestId: string): Promise { return HttpClient.post('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/reject', this.buildAppBody()) } async sendGroupJoinRequest(groupId: string, remark: string | null = null): Promise { const params = new GroupJoinRequestBody() params.appId = SDKContext.getConfig().appKey if (remark !== null && remark !== '') { params.remark = remark } return HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', params) } async listGroupJoinRequests(groupId: string): Promise { return HttpClient.get('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', this.buildAppQuery()) } async acceptGroupJoinRequest(groupId: string, requestId: string): Promise { return HttpClient.post( '/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/accept', this.buildAppBody(), ) } async rejectGroupJoinRequest(groupId: string, requestId: string): Promise { return HttpClient.post( '/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/reject', this.buildAppBody(), ) } async getProfile(userId: string): Promise { return HttpClient.get('/api/im/accounts/' + encodeURIComponent(userId), this.buildAppQuery()) } async updateProfile( userId: string, nickname: string | null = null, avatar: string | null = null, gender: string | null = null, ): Promise { const params = new UpdateProfileBody() params.appId = SDKContext.getConfig().appKey if (nickname !== null) { params.nickname = nickname } if (avatar !== null) { params.avatar = avatar } if (gender !== null) { params.gender = gender } return HttpClient.put('/api/im/accounts/' + encodeURIComponent(userId), params) } /* ---------- 会话扩展 ---------- */ async setConversationHidden(targetId: string, hidden: boolean): Promise { const params = new AppBody() params.appId = SDKContext.getConfig().appKey await HttpClient.put('/api/im/conversations/' + encodeURIComponent(targetId) + '/hidden', params) } async setConversationGroup(targetId: string, groupName: string): Promise { const params = { appId: SDKContext.getConfig().appKey, groupName } await HttpClient.put('/api/im/conversations/' + encodeURIComponent(targetId) + '/group', params) } async listConversationGroups(): Promise { return HttpClient.get('/api/im/conversation-groups', this.buildAppQuery()) } async listConversationGroupItems(groupName: string): Promise { return HttpClient.get('/api/im/conversation-groups/' + encodeURIComponent(groupName), this.buildAppQuery()) } /* ---------- 好友扩展 ---------- */ async addFriend(friendId: string): Promise { const params = { appId: SDKContext.getConfig().appKey, friendId } await HttpClient.post('/api/im/friends', params) } async removeFriend(friendId: string): Promise { await HttpClient.delete('/api/im/friends/' + encodeURIComponent(friendId), this.buildAppQuery()) } async removeAllFriends(): Promise { await HttpClient.delete('/api/im/friends', this.buildAppQuery()) } async setFriendGroup(friendId: string, groupName: string): Promise { const params = { appId: SDKContext.getConfig().appKey, groupName } await HttpClient.put('/api/im/friends/' + encodeURIComponent(friendId) + '/group', params) } async listFriendGroups(): Promise { return HttpClient.get('/api/im/friends/groups', this.buildAppQuery()) } async listFriendsByGroup(groupName: string): Promise { return HttpClient.get('/api/im/friends/groups/' + encodeURIComponent(groupName), this.buildAppQuery()) } async checkFriends(friendIds: string[]): Promise> { const params = { appId: SDKContext.getConfig().appKey, friendIds } return HttpClient.post>('/api/im/friends/check', params) } /* ---------- 黑名单 ---------- */ async listBlacklist(): Promise { return HttpClient.get('/api/im/blacklist', this.buildAppQuery()) } async addToBlacklist(userId: string): Promise { const params = { appId: SDKContext.getConfig().appKey, userId } await HttpClient.post('/api/im/blacklist', params) } async removeFromBlacklist(userId: string): Promise { await HttpClient.delete('/api/im/blacklist', this.buildAppQuery() + '&userId=' + encodeURIComponent(userId)) } async checkBlacklist(userId: string): Promise<{ meBlocked: boolean; theyBlocked: boolean; eitherBlocked: boolean }> { return HttpClient.get<{ meBlocked: boolean; theyBlocked: boolean; eitherBlocked: boolean }>( '/api/im/blacklist/check', this.buildAppQuery() + '&userId=' + encodeURIComponent(userId), ) } /* ---------- 群组扩展 ---------- */ async createGroup(name: string, memberIds: string[], groupType: string = 'WORK'): Promise { const params = { appId: SDKContext.getConfig().appKey, name, memberIds, groupType } return HttpClient.post('/api/im/groups', params) } async updateGroupInfo(groupId: string, name?: string, announcement?: string): Promise { const params: Record = { appId: SDKContext.getConfig().appKey } if (name !== undefined) params.name = name if (announcement !== undefined) params.announcement = announcement return HttpClient.put('/api/im/groups/' + encodeURIComponent(groupId), params) } async addGroupMember(groupId: string, userId: string): Promise { const params = { appId: SDKContext.getConfig().appKey, userId } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/members', params) } async removeGroupMember(groupId: string, userId: string): Promise { await HttpClient.delete('/api/im/groups/' + encodeURIComponent(groupId) + '/members/' + encodeURIComponent(userId), this.buildAppQuery()) } async leaveGroup(groupId: string): Promise { await HttpClient.delete('/api/im/groups/' + encodeURIComponent(groupId) + '/members/me', this.buildAppQuery()) } async setGroupRole(groupId: string, userId: string, role: 'ADMIN' | 'MEMBER'): Promise { const params = { appId: SDKContext.getConfig().appKey, userId, role } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/roles', params) } async muteGroupMember(groupId: string, userId: string, muted: boolean): Promise { const params = { appId: SDKContext.getConfig().appKey, userId, muted } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/mute', params) } async transferGroupOwner(groupId: string, newOwnerId: string): Promise { const params = { appId: SDKContext.getConfig().appKey, newOwnerId } return HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/owner', params) } async updateGroupAttributes(groupId: string, attributes: Record): Promise { const params = { appId: SDKContext.getConfig().appKey, attributes } await HttpClient.put('/api/im/groups/' + encodeURIComponent(groupId) + '/attributes', params) } async removeGroupAttributes(groupId: string, keys: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, keys } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/attributes/delete', params) } async dismissGroup(groupId: string): Promise { await HttpClient.delete('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery()) } /* ---------- 批量操作 ---------- */ async batchAddFriends(friendIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, friendIds } await HttpClient.post('/api/im/friends/batch', params) } async batchRemoveFriends(friendIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, friendIds } await HttpClient.post('/api/im/friends/batch/remove', params) } async batchAcceptFriendRequests(requestIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, requestIds } await HttpClient.post('/api/im/friend-requests/batch/accept', params) } async batchRejectFriendRequests(requestIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, requestIds } await HttpClient.post('/api/im/friend-requests/batch/reject', params) } async batchAddMembers(groupId: string, userIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, userIds } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/members/batch', params) } async batchRemoveMembers(groupId: string, userIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, userIds } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/members/batch/remove', params) } async batchAcceptJoinRequests(groupId: string, requestIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, requestIds } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/batch/accept', params) } async batchRejectJoinRequests(groupId: string, requestIds: string[]): Promise { const params = { appId: SDKContext.getConfig().appKey, requestIds } await HttpClient.post('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/batch/reject', params) } async modifyMemberInfo(groupId: string, userId: string, nickname?: string, role?: string): Promise { const params: Record = { appId: SDKContext.getConfig().appKey } if (nickname !== undefined) params.nickname = nickname if (role !== undefined) params.role = role await HttpClient.put('/api/im/groups/' + encodeURIComponent(groupId) + '/members/' + encodeURIComponent(userId) + '/info', params) } private buildAppBody(): AppBody { const body = new AppBody() body.appId = SDKContext.getConfig().appKey return body } private buildAppQuery(): string { return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) } private buildConversationActionQuery(chatType: ChatType): string { return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&chatType=' + encodeURIComponent(chatType) } private buildConversationQuery(size: number): string { return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&page=0&size=' + encodeURIComponent(size) } private buildFriendRequestQuery(direction: 'incoming' | 'outgoing'): string { return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&direction=' + encodeURIComponent(direction) } private buildSearchQuery(keyword: string, size: number): string { return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&keyword=' + encodeURIComponent(keyword) + '&size=' + encodeURIComponent(size) } private buildMessageSearchQuery( keyword: string, chatType: ChatType | '', msgType: MsgType | '', page: number, size: number, ): string { const parts: string[] = [] parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey)) if (keyword !== '') { parts.push('keyword=' + encodeURIComponent(keyword)) } if (chatType !== '') { parts.push('chatType=' + encodeURIComponent(chatType)) } if (msgType !== '') { parts.push('msgType=' + encodeURIComponent(msgType)) } parts.push('page=' + encodeURIComponent(page)) parts.push('size=' + encodeURIComponent(size)) return parts.join('&') } private buildHistoryQuery(page: number, size: number, query: HistoryQuery): string { const parts: string[] = [] parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey)) parts.push('page=' + encodeURIComponent(page)) parts.push('size=' + encodeURIComponent(size)) if (query.msgType !== undefined) { parts.push('msgType=' + encodeURIComponent(query.msgType)) } if (query.keyword !== undefined && query.keyword !== '') { parts.push('keyword=' + encodeURIComponent(query.keyword)) } if (query.startTime !== undefined) { const startValue = query.startTime instanceof Date ? this.formatDateTime(query.startTime) : String(query.startTime) parts.push('startTime=' + encodeURIComponent(startValue)) } if (query.endTime !== undefined) { const endValue = query.endTime instanceof Date ? this.formatDateTime(query.endTime) : String(query.endTime) parts.push('endTime=' + encodeURIComponent(endValue)) } return parts.join('&') } async sendImageMessage(toId: string, chatType: ChatType, filePath: string, width?: number, height?: number): Promise { const uploadResult = await this.uploadFile(filePath) const url = (uploadResult as Record)['url'] ?? '' const thumbnailUrl = (uploadResult as Record)['thumbnailUrl'] const contentObj: Record = { url } if (width !== undefined) contentObj.width = width if (height !== undefined) contentObj.height = height if (thumbnailUrl) contentObj.thumbnailUrl = thumbnailUrl return this.send({ toId, chatType, msgType: 'IMAGE', content: JSON.stringify(contentObj) }) } async sendVideoMessage(toId: string, chatType: ChatType, filePath: string, width?: number, height?: number, duration?: number): Promise { const uploadResult = await this.uploadFile(filePath) const url = (uploadResult as Record)['url'] ?? '' const thumbnailUrl = (uploadResult as Record)['thumbnailUrl'] const contentObj: Record = { url } if (width !== undefined) contentObj.width = width if (height !== undefined) contentObj.height = height if (duration !== undefined) contentObj.duration = duration if (thumbnailUrl) contentObj.thumbnailUrl = thumbnailUrl return this.send({ toId, chatType, msgType: 'VIDEO', content: JSON.stringify(contentObj) }) } async sendFileMessage(toId: string, chatType: ChatType, filePath: string): Promise { const uploadResult = await this.uploadFile(filePath) const url = (uploadResult as Record)['url'] ?? '' const name = (uploadResult as Record)['originalName'] ?? '' const size = (uploadResult as Record)['size'] ?? 0 const contentObj: Record = { url, name, size } return this.send({ toId, chatType, msgType: 'FILE', content: JSON.stringify(contentObj) }) } async sendAudioMessage(toId: string, chatType: ChatType, filePath: string, duration?: number): Promise { const uploadResult = await this.uploadFile(filePath) const url = (uploadResult as Record)['url'] ?? '' const contentObj: Record = { url } if (duration !== undefined) contentObj.duration = duration return this.send({ toId, chatType, msgType: 'AUDIO', content: JSON.stringify(contentObj) }) } private async uploadFile(filePath: string): Promise { const token = SDKContext.getToken() const url = DEFAULT_API_BASE_URL.replace(/\/$/, '') + '/api/file/upload' const client = http.createHttp() try { const header: Record = {} if (token) header.Authorization = 'Bearer ' + token const multipartForm = new http.MultipartForm() multipartForm.addFile('file', filePath, filePath.substring(filePath.lastIndexOf('/') + 1)) const options: http.HttpRequestOptions = { method: http.RequestMethod.POST, header, extraData: multipartForm, connectTimeout: 60000, readTimeout: 60000, } const res = await client.request(url, options) const json = JSON.parse(res.result as string) as { code: number; message: string; data: Object } if (json.code !== 200) throw new Error(json.message) return json.data } finally { client.destroy() } } private async notifyConversations(): Promise { try { const conversations = await this.listConversations() this.delegate?.onConversationsChange?.(conversations) } catch { // ignore } } disconnect(): void { this.destroyed = true if (this.reconnectTimer !== null) { clearTimeout(this.reconnectTimer) this.reconnectTimer = null } this.ws?.close((_err: Error) => {}) this.ws = null } private scheduleReconnect(): void { if (this.destroyed) return const delay = this.reconnectDelay if (SDKContext.getConfig().debug) console.log('[ImClient] reconnect in ' + delay + 'ms') this.reconnectTimer = setTimeout(() => { this.connect() this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY) }, delay) } private buildOutgoingMessage(params: SendMessageParams): ImMessage { const messageId = params.messageId ?? this.generateMessageId() const userId = SDKContext.getUserId() ?? '' const appId = SDKContext.getConfig().appKey const message: ImMessage = { id: messageId, appId, fromUserId: userId, fromId: userId, toId: params.toId, chatType: params.chatType, msgType: params.msgType, content: params.content, status: 'SENDING', mentionedUserIds: params.mentionedUserIds, groupReadCount: 0, revoked: false, createdAt: Date.now(), } return message } private markFailed(message: ImMessage): ImMessage { const failed: ImMessage = { id: message.id, appId: message.appId, fromUserId: message.fromUserId, fromId: message.fromId, toId: message.toId, chatType: message.chatType, msgType: message.msgType, content: message.content, status: 'FAILED', mentionedUserIds: message.mentionedUserIds, groupReadCount: message.groupReadCount, revoked: message.revoked, createdAt: message.createdAt, } return failed } private normalizeMessage(message: ImMessage): ImMessage { const normalized: ImMessage = { id: message.id, appId: message.appId || SDKContext.getConfig().appKey, fromUserId: message.fromUserId, fromId: message.fromId || message.fromUserId, toId: message.toId, chatType: message.chatType, msgType: message.msgType, content: message.content, status: message.status, mentionedUserIds: message.mentionedUserIds, groupReadCount: message.groupReadCount, revoked: message.revoked || message.status === 'REVOKED', createdAt: message.createdAt, } return normalized } private generateMessageId(): string { return 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000000).toString(16) } private formatDateTime(value: Date): string { const pad = (n: number): string => { return n < 10 ? '0' + n : String(n) } return [ value.getFullYear(), '-', pad(value.getMonth() + 1), '-', pad(value.getDate()), 'T', pad(value.getHours()), ':', pad(value.getMinutes()), ':', pad(value.getSeconds()), ].join('') } private toStringValue(value: Date | string | number | undefined): string | undefined { if (value === undefined) { return undefined } if (value instanceof Date) { return this.formatDateTime(value) } return String(value) } }