feat(im): 添加即时通讯功能模块

- 添加了 IM API 接口定义,包含登录、消息、群组、好友等接口
- 实现了 ImSDK 核心功能,支持发送各类消息和管理会话
- 集成了 WebSocket 连接管理和自动重连机制
- 添加了本地联系人缓存并优化对话标题显示逻辑
- 实现了 HarmonyOS 平台 HTTP 客户端基础功能
这个提交包含在:
XuqmGroup 2026-04-28 16:55:12 +08:00
父节点 2ca58aa458
当前提交 765d8a0333
共有 8 个文件被更改,包括 214 次插入24 次删除

查看文件

@ -16,6 +16,7 @@ export interface XuqmConfig {
}
let _config: XuqmConfig | null = null
let _userId: string | null = null
export function initConfigFromRemote(
options: XuqmInitOptions,
@ -37,6 +38,14 @@ export function getConfig(): XuqmConfig {
return _config
}
export function setUserId(userId: string | null): void {
_userId = userId
}
export function getUserId(): string | null {
return _userId
}
export function isInitialized(): boolean {
return _config !== null
}

查看文件

@ -1,6 +1,6 @@
export { XuqmSDK } from './sdk'
export type { XuqmInitOptions, XuqmConfig } from './config'
export { getConfig, isInitialized } from './config'
export { getConfig, isInitialized, setUserId, getUserId } from './config'
export { apiRequest, _getToken, _saveToken, _clearToken } from './http'
export { API_BASE_URL, IM_WS_URL } from './constants'
export { getDeviceId, getDeviceInfo, detectPushVendor } from './device'

查看文件

@ -1,4 +1,4 @@
import { initConfigFromRemote, isInitialized, type XuqmInitOptions } from './config'
import { initConfigFromRemote, isInitialized, type XuqmInitOptions, setUserId as setCommonUserId, getUserId as getCommonUserId } from './config'
export const XuqmSDK = {
/**
@ -50,4 +50,12 @@ export const XuqmSDK = {
apiBaseUrl: options.serverUrl,
})
},
setUserId(userId: string | null): void {
setCommonUserId(userId)
},
getUserId(): string | null {
return getCommonUserId()
},
}

查看文件

@ -1,4 +1,4 @@
import { _getToken, getConfig } from '@xuqm/rn-common'
import { _getToken, getConfig, getUserId } from '@xuqm/rn-common'
import type { ImEventListener, ImMessage, SendMessageParams } from './types'
interface StompFrame {
@ -50,8 +50,8 @@ export class ImClient {
msgType: SendMessageParams['msgType'],
content: string,
mentionedUserIds?: string,
) {
this.send({
): ImMessage {
return this.send({
toId,
chatType,
msgType,
@ -60,13 +60,14 @@ export class ImClient {
})
}
send(params: SendMessageParams) {
if (this.ws?.readyState !== WebSocket.OPEN) {
throw new Error('IM not connected')
}
send(params: SendMessageParams): ImMessage {
if (!this.activeAppId) {
throw new Error('IM appId not configured')
}
const outgoing = this.buildOutgoingMessage(params)
if (this.ws?.readyState !== WebSocket.OPEN) {
return { ...outgoing, status: 'FAILED' }
}
this.sendFrame(
'SEND',
@ -76,6 +77,7 @@ export class ImClient {
},
JSON.stringify({
appId: this.activeAppId,
messageId: outgoing.id,
toId: params.toId,
chatType: params.chatType,
msgType: params.msgType,
@ -83,6 +85,8 @@ export class ImClient {
mentionedUserIds: params.mentionedUserIds ?? '',
}),
)
return outgoing
}
revoke(messageId: string) {
@ -205,7 +209,7 @@ export class ImClient {
}
if (frame.command === 'MESSAGE') {
const message: ImMessage = JSON.parse(frame.body)
const message: ImMessage = this.normalizeMessage(JSON.parse(frame.body))
if (message.chatType === 'GROUP') {
this.listeners.forEach(listener => listener.onGroupMessage?.(message))
return
@ -253,4 +257,38 @@ export class ImClient {
return { command, headers, body }
})
}
private buildOutgoingMessage(params: SendMessageParams): ImMessage {
const userId = getUserId() ?? ''
const messageId = params.messageId ?? this.generateMessageId()
return {
id: messageId,
appId: this.activeAppId ?? getConfig().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(),
}
}
private generateMessageId(): string {
const cryptoId = globalThis.crypto?.randomUUID?.()
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
}
private normalizeMessage(message: ImMessage): ImMessage {
return {
...message,
fromId: message.fromId ?? message.fromUserId,
revoked: message.revoked ?? message.status === 'REVOKED',
appId: message.appId ?? (this.activeAppId ?? getConfig().appId),
}
}
}

查看文件

@ -1,4 +1,4 @@
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo } from '@xuqm/rn-common'
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId } from '@xuqm/rn-common'
import { ImClient } from './ImClient'
import { ImDatabase } from './db/ImDatabase'
import type { MessageSearchParams } from './db/ImDatabase'
@ -12,12 +12,56 @@ import type {
ImGroup,
ImMessage,
MsgType,
UserProfile,
} from './types'
import { uploadFile } from './upload'
let client: ImClient | null = null
let _currentUserId: string | null = null
function generateMessageId(): string {
const cryptoId = globalThis.crypto?.randomUUID?.()
return cryptoId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
}
function normalizeMessage(msg: ImMessage, fallback?: Partial<ImMessage>): ImMessage {
return {
...fallback,
...msg,
appId: msg.appId ?? fallback?.appId ?? getConfig().appId,
fromId: msg.fromId ?? fallback?.fromId ?? msg.fromUserId,
revoked: msg.revoked ?? msg.status === 'REVOKED',
}
}
function buildOutgoingMessage(params: {
messageId?: string
toId: string
chatType: ChatType
msgType: MsgType
content: string
mentionedUserIds?: string
}): ImMessage {
const config = getConfig()
const fromId = _currentUserId ?? getCommonUserId() ?? ''
const id = params.messageId ?? generateMessageId()
return {
id,
appId: config.appId,
fromUserId: fromId,
fromId,
toId: params.toId,
chatType: params.chatType,
msgType: params.msgType,
content: params.content,
status: 'SENDING',
mentionedUserIds: params.mentionedUserIds,
groupReadCount: 0,
revoked: false,
createdAt: Date.now(),
}
}
async function _syncHistoryForAllConversations(): Promise<void> {
if (!ImDatabase.isInitialized() || !_currentUserId) return
@ -124,6 +168,7 @@ export const ImSDK = {
})
await _saveToken(res.token)
_currentUserId = userId
setCommonUserId(userId)
if (dbName !== undefined || ImDatabase.isInitialized()) {
ImDatabase.init(dbName ?? 'xuqm_im')
@ -146,6 +191,7 @@ export const ImSDK = {
const config = getConfig()
await _saveToken(token)
_currentUserId = userId
setCommonUserId(userId)
if (dbName !== undefined || ImDatabase.isInitialized()) {
ImDatabase.init(dbName ?? 'xuqm_im')
@ -294,15 +340,38 @@ export const ImSDK = {
mentionedUserIds?: string,
): Promise<ImMessage> {
const config = getConfig()
const outgoing = buildOutgoingMessage({
toId,
chatType,
msgType,
content,
mentionedUserIds,
})
try {
const msg = await apiRequest<ImMessage>('/api/im/messages/send', {
method: 'POST',
params: { appId: config.appId },
body: { toId, chatType, msgType, content, mentionedUserIds: mentionedUserIds ?? '' },
body: {
toId,
chatType,
msgType,
content,
mentionedUserIds: mentionedUserIds ?? '',
messageId: outgoing.id,
},
})
const finalMsg = normalizeMessage(msg, outgoing)
if (ImDatabase.isInitialized() && _currentUserId) {
await ImDatabase.saveMessage(msg, _currentUserId)
await ImDatabase.saveMessage(finalMsg, _currentUserId)
}
return finalMsg
} catch (error) {
const failed = { ...outgoing, status: 'FAILED' as const }
if (ImDatabase.isInitialized() && _currentUserId) {
await ImDatabase.saveMessage(failed, _currentUserId)
}
return failed
}
return msg
},
async sendTextMessage(
@ -457,7 +526,7 @@ export const ImSDK = {
params: { appId: config.appId },
})
if (ImDatabase.isInitialized() && _currentUserId) {
await ImDatabase.saveMessage(msg, _currentUserId)
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
}
return msg
},
@ -669,6 +738,31 @@ export const ImSDK = {
})
},
async getProfile(userId: string): Promise<UserProfile> {
const config = getConfig()
return apiRequest<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
params: { appId: config.appId },
})
},
async updateProfile(
userId: string,
nickname?: string,
avatar?: string,
gender?: string,
): Promise<UserProfile> {
const config = getConfig()
return apiRequest<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
method: 'PUT',
params: {
appId: config.appId,
...(nickname ? { nickname } : {}),
...(avatar ? { avatar } : {}),
...(gender ? { gender } : {}),
},
})
},
async listConversations(): Promise<ConversationData[]> {
const config = getConfig()
if (ImDatabase.isInitialized()) {
@ -802,15 +896,15 @@ export const ImSDK = {
...listener,
onMessage: async (msg) => {
if (ImDatabase.isInitialized() && _currentUserId) {
await ImDatabase.saveMessage(msg, _currentUserId)
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
}
listener.onMessage?.(msg)
listener.onMessage?.(normalizeMessage(msg))
},
onGroupMessage: async (msg) => {
if (ImDatabase.isInitialized() && _currentUserId) {
await ImDatabase.saveMessage(msg, _currentUserId)
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
}
listener.onGroupMessage?.(msg)
listener.onGroupMessage?.(normalizeMessage(msg))
},
})
},
@ -835,5 +929,6 @@ export const ImSDK = {
client?.disconnect()
client = null
_currentUserId = null
setCommonUserId(null)
},
}

查看文件

@ -12,9 +12,12 @@ export type {
ImMessage, ImGroup, ChatType, MsgType, MsgStatus,
ImEventListener, SendMessageParams,
ConversationData,
HistoryQuery,
PageResult,
FriendRequest,
GroupJoinRequest,
BlacklistEntry,
UserProfile,
} from './types'
export { uploadFile } from './upload'
export type { UploadResult } from './upload'

查看文件

@ -23,6 +23,7 @@ export interface ImMessage {
id: string
appId: string
fromUserId: string
fromId?: string
toId: string
chatType: ChatType
msgType: MsgType
@ -30,6 +31,7 @@ export interface ImMessage {
status: MsgStatus
mentionedUserIds?: string
groupReadCount?: number
revoked?: boolean
createdAt: number
}
@ -42,6 +44,7 @@ export interface ImEventListener {
}
export interface SendMessageParams {
messageId?: string
toId: string
chatType: ChatType
msgType: MsgType
@ -72,6 +75,27 @@ export interface ConversationData {
isPinned: boolean
}
export interface HistoryQuery {
msgType?: MsgType
keyword?: string
startTime?: Date | string | number
endTime?: Date | string | number
page?: number
size?: number
}
export interface PageResult<T> {
content: T[]
totalElements: number
totalPages: number
size: number
number: number
numberOfElements: number
first: boolean
last: boolean
empty: boolean
}
export interface FriendRequest {
id: string
appId: string
@ -101,3 +125,14 @@ export interface BlacklistEntry {
blockedUserId: string
createdAt: number
}
export interface UserProfile {
id?: string
appId?: string
userId: string
nickname?: string | null
avatar?: string | null
gender?: string | null
status?: string | null
createdAt?: number | null
}

查看文件

@ -4,7 +4,7 @@
export { XuqmSDK } from '../packages/common/src'
export type { XuqmInitOptions, DeviceInfo } from '../packages/common/src'
export { getDeviceId, getDeviceInfo, detectPushVendor } from '../packages/common/src'
export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId } from '../packages/common/src'
export { ScaledImage } from '../packages/common/src'
export { ImSDK } from '../packages/im/src'
@ -15,6 +15,8 @@ export type {
ImMessage, ImGroup, ChatType, MsgType, MsgStatus,
ImEventListener, SendMessageParams,
ConversationData,
HistoryQuery,
PageResult,
FriendRequest,
GroupJoinRequest,
BlacklistEntry,