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

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

查看文件

@ -19,6 +19,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
private var activeWsURL: URL?
private var activeToken: String?
private var activeAppId: String?
private var activeUserId: String?
public init(wsURL: URL? = nil, token: String? = nil, appId: String? = nil) {
self.wsURLOverride = wsURL
@ -27,6 +28,10 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
super.init()
}
public func setCurrentUserId(_ userId: String) {
activeUserId = userId
}
public func connect() {
shouldReconnect = true
reconnectWorkItem?.cancel()
@ -55,7 +60,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
self.activeToken = activeToken
}
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) -> ImMessage {
sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content, mentionedUserIds: nil)
}
@ -65,12 +70,27 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
msgType: MsgType,
content: String,
mentionedUserIds: String?
) {
) -> ImMessage {
let messageId = UUID().uuidString
let now = Int64(Date().timeIntervalSince1970 * 1000)
let message = ImMessage(
id: messageId,
appId: activeAppId ?? "",
fromUserId: activeUserId ?? "",
toId: toId,
chatType: chatType,
msgType: msgType,
content: content,
status: .sending,
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
groupReadCount: nil,
createdAt: now
)
guard let activeAppId else {
delegate?.imClientDidError("IM appId not configured")
return
return message.failedCopy()
}
sendFrame(
let sent = sendFrame(
command: "SEND",
headers: [
"destination": "/app/chat.send",
@ -78,6 +98,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
],
body: encodeJSONString([
"appId": activeAppId,
"messageId": messageId,
"toId": toId,
"chatType": chatType.rawValue,
"msgType": msgType.rawValue,
@ -85,6 +106,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
"mentionedUserIds": mentionedUserIds ?? "",
]),
)
return sent ? message : message.failedCopy()
}
public func revoke(messageId: String) {
@ -202,13 +224,14 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
DispatchQueue.global().asyncAfter(deadline: .now() + reconnectDelay, execute: workItem)
}
private func sendFrame(command: String, headers: [String: String] = [:], body: String = "") {
guard let webSocketTask else { return }
private func sendFrame(command: String, headers: [String: String] = [:], body: String = "") -> Bool {
guard let webSocketTask, webSocketTask.state == .running else { return false }
let headerLines = headers.map { "\($0.key):\($0.value)" }.joined(separator: "\n")
let frame = headerLines.isEmpty
? "\(command)\n\n\(body)\u{0000}"
: "\(command)\n\(headerLines)\n\n\(body)\u{0000}"
webSocketTask.send(.string(frame)) { _ in }
return true
}
private func encodeJSONString(_ object: [String: String]) -> String {
@ -262,6 +285,24 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
}
}
private extension ImMessage {
func failedCopy() -> ImMessage {
ImMessage(
id: id,
appId: appId,
fromUserId: fromUserId,
toId: toId,
chatType: chatType,
msgType: msgType,
content: content,
status: .failed,
mentionedUserIds: mentionedUserIds,
groupReadCount: groupReadCount,
createdAt: createdAt
)
}
}
private struct StompFrame {
let command: String
let headers: [String: String]

查看文件

@ -6,6 +6,7 @@ public final class ImSDK {
public static let shared = ImSDK()
private var client: ImClient?
private weak var delegate: ImEventDelegate?
private var currentUserId: String?
private init() {}
@ -24,9 +25,11 @@ public final class ImSDK {
method: "POST",
queryItems: items
)
currentUserId = userId
XuqmSDK.shared.tokenStore?.save(res.token)
client?.disconnect()
client = ImClient(wsURL: config.imWebSocketURL, token: res.token, appId: config.appId)
client?.setCurrentUserId(userId)
client?.delegate = delegate
client?.connect()
}
@ -36,11 +39,12 @@ public final class ImSDK {
client?.delegate = delegate
}
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
public func sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) -> ImMessage {
client?.sendMessage(toId: toId, chatType: chatType, msgType: msgType, content: content)
?? fallbackFailedMessage(toId: toId, chatType: chatType, msgType: msgType, content: content, mentionedUserIds: nil)
}
public func sendTextMessage(toId: String, chatType: ChatType, content: String) {
public func sendTextMessage(toId: String, chatType: ChatType, content: String) -> ImMessage {
sendMessage(toId: toId, chatType: chatType, msgType: .text, content: content)
}
@ -52,7 +56,7 @@ public final class ImSDK {
width: Int? = nil,
height: Int? = nil,
size: Int64? = nil
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -74,7 +78,7 @@ public final class ImSDK {
thumbnailUrl: String? = nil,
duration: Int64? = nil,
size: Int64? = nil
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -94,7 +98,7 @@ public final class ImSDK {
url: String,
duration: Int64? = nil,
size: Int64? = nil
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -113,7 +117,7 @@ public final class ImSDK {
url: String,
name: String,
size: Int64
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -133,7 +137,7 @@ public final class ImSDK {
lng: Double,
title: String? = nil,
address: String? = nil
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -147,7 +151,7 @@ public final class ImSDK {
)
}
public func sendCustomMessage(toId: String, chatType: ChatType, data: String) {
public func sendCustomMessage(toId: String, chatType: ChatType, data: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -156,7 +160,7 @@ public final class ImSDK {
)
}
public func sendRichTextMessage(toId: String, chatType: ChatType, html: String) {
public func sendRichTextMessage(toId: String, chatType: ChatType, html: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -165,7 +169,7 @@ public final class ImSDK {
)
}
public func sendNotifyMessage(toId: String, chatType: ChatType, title: String, content: String) {
public func sendNotifyMessage(toId: String, chatType: ChatType, title: String, content: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -180,7 +184,7 @@ public final class ImSDK {
quotedMsgId: String,
quotedContent: String,
text: String
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -198,7 +202,7 @@ public final class ImSDK {
chatType: ChatType,
title: String,
msgList: [String]
) {
) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -210,7 +214,7 @@ public final class ImSDK {
)
}
public func sendForwardMessage(toId: String, chatType: ChatType, originalSender: String, originalContent: String) {
public func sendForwardMessage(toId: String, chatType: ChatType, originalSender: String, originalContent: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -222,7 +226,7 @@ public final class ImSDK {
)
}
public func sendCallAudioMessage(toId: String, chatType: ChatType, action: String) {
public func sendCallAudioMessage(toId: String, chatType: ChatType, action: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -231,7 +235,7 @@ public final class ImSDK {
)
}
public func sendCallVideoMessage(toId: String, chatType: ChatType, action: String) {
public func sendCallVideoMessage(toId: String, chatType: ChatType, action: String) -> ImMessage {
sendMessage(
toId: toId,
chatType: chatType,
@ -252,6 +256,28 @@ public final class ImSDK {
client?.unsubscribeGroup(groupId)
}
private func fallbackFailedMessage(
toId: String,
chatType: ChatType,
msgType: MsgType,
content: String,
mentionedUserIds: String?
) -> ImMessage {
ImMessage(
id: UUID().uuidString,
appId: XuqmSDK.shared.requireConfig().appId,
fromUserId: currentUserId ?? "",
toId: toId,
chatType: chatType,
msgType: msgType,
content: content,
status: .failed,
mentionedUserIds: mentionedUserIds,
groupReadCount: nil,
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
)
}
public func fetchHistory(
toId: String,
page: Int = 0,
@ -521,6 +547,32 @@ public final class ImSDK {
)
}
public func getProfile(userId: String) async throws -> UserProfile {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/accounts/\(userId)",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func updateProfile(
userId: String,
nickname: String? = nil,
avatar: String? = nil,
gender: String? = nil
) async throws -> UserProfile {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
if let nickname { items.append(URLQueryItem(name: "nickname", value: nickname)) }
if let avatar { items.append(URLQueryItem(name: "avatar", value: avatar)) }
if let gender { items.append(URLQueryItem(name: "gender", value: gender)) }
return try await ApiClient.shared.request(
path: "/api/im/accounts/\(userId)",
method: "PUT",
queryItems: items
)
}
public func listConversations() async throws -> [ConversationData] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(

查看文件

@ -107,7 +107,19 @@ public struct BlacklistEntry: Codable, Sendable {
public let createdAt: Int64
}
public struct UserProfile: Codable, Sendable {
public let id: String?
public let appId: String?
public let userId: String
public let nickname: String?
public let avatar: String?
public let gender: String?
public let status: String?
public let createdAt: Int64?
}
public struct ImSendMessageRequest: Encodable, Sendable {
public let messageId: String
public let toId: String
public let chatType: ChatType
public let msgType: MsgType