findme-miniprogram-frontend/utils/nim-conversation-manager.js
2025-12-27 17:16:03 +08:00

987 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* NIM SDK 会话管理器
* 负责会话列表的获取、监听和管理、消息发送
* 基于网易云信 NIM SDK v10.9.50
*/
import { NIM, V2NIMConst } from '../dist/nim'
class NIMConversationManager {
constructor() {
this.nim = null
this.listeners = new Map() // 事件监听器集合
this.conversations = [] // 会话列表缓存
this.isInitialized = false
}
/**
* 初始化会话管理器
* @param {Object} nimInstance - NIM SDK 实例
*/
init(nimInstance) {
if (this.isInitialized) {
console.warn('⚠️ NIM会话管理器已经初始化')
return
}
this.nim = nimInstance
this.isInitialized = true
// 注册 NIM SDK 事件监听
this.registerNIMEvents()
console.log('✅ NIM会话管理器初始化成功')
}
/**
* 注册 NIM SDK 事件监听
*/
registerNIMEvents() {
if (!this.nim) {
console.error('❌ NIM实例不存在无法注册事件')
return
}
try {
console.log('📡 开始注册 NIM SDK 事件监听器...')
// 🔥 使用 addEventListener 方法注册会话监听器(官方推荐方式)
// 会话新增
this.nim.V2NIMConversationService.on('onConversationCreated', (conversation) => {
console.log('📝 NIM 会话新增:', conversation)
this.handleConversationCreated(conversation)
})
// 会话删除
this.nim.V2NIMConversationService.on('onConversationDeleted', (conversationIds) => {
console.log('🗑️ NIM 会话删除:', conversationIds)
this.handleConversationDeleted(conversationIds)
})
// 会话更新
this.nim.V2NIMConversationService.on('onConversationChanged', (conversations) => {
console.log('🔄 NIM 会话更新:', conversations)
this.handleConversationChanged(conversations)
})
// 🔥 使用 addEventListener 方法注册消息监听器(官方推荐方式)
// 接收新消息
this.nim.V2NIMMessageService.on('onReceiveMessages', (messages) => {
console.log('📨 NIM 收到新消息,数量:', messages?.length || 0, messages)
this.handleNewMessages(messages)
})
// 消息发送状态
this.nim.V2NIMMessageService.on('onSendMessage', (message) => {
console.log('📤 NIM 消息发送中:', message)
})
// 消息撤回
this.nim.V2NIMMessageService.on('onMessageRevokeNotifications', (revokeNotifications) => {
console.log('↩️ NIM 消息撤回通知:', revokeNotifications)
this.handleMessageRevoke(revokeNotifications)
})
console.log('✅ NIM事件监听注册成功')
} catch (error) {
console.error('❌ 注册NIM事件监听失败:', error)
}
}
/**
* 获取会话列表
* @param {Number} offset - 偏移
* @param {Number} limit - 分页
* @returns {Promise<Array>} 会话列表
*/
async getConversationList(offset = 0, limit = 10) {
if (!this.nim) {
throw new Error('NIM实例未初始化')
}
try {
console.log('📋 开始获取NIM云端会话列表...')
// 🔥 使用云端会话服务获取会话列表
const result = await this.nim.V2NIMConversationService.getConversationList(
offset, limit
// {
// offset: options.offset || 0,
// limit: options.limit || 100
// }
)
console.log('✅ 获取云端会话列表成功:', result)
// 转换为统一格式
const conversations = this.normalizeConversations(result?.conversationList || [])
// 更新缓存
this.conversations = conversations
return conversations
} catch (error) {
console.error('❌ 获取云端会话列表失败:', error)
throw error
}
}
/**
* 将 NIM 会话数据转换为应用统一格式
* @param {Array} nimConversations - NIM会话数据
* @returns {Array} 标准化的会话列表
*/
normalizeConversations(nimConversations) {
if (!Array.isArray(nimConversations)) {
return []
}
return nimConversations.map(conv => this.normalizeConversation(conv))
}
/**
* 标准化单个会话
* @param {Object} conv - NIM会话对象
* @returns {Object} 标准化的会话对象
*/
normalizeConversation(conv) {
if (!conv) return null
// 解析会话类型
const conversationType = conv.conversationType || conv.type
const isGroupChat = conversationType === V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
// 获取最后一条消息
const lastMessage = conv.lastMessage || {}
const lastMessageText = this.getMessagePreview(lastMessage)
// 🔥 正确获取时间V2NIMLastMessage.messageRefer.createTime
const messageRefer = lastMessage.messageRefer || {}
const messageTime = messageRefer.createTime || conv.updateTime || 0
return {
// 基础信息
id: conv.conversationId,
conversationId: conv.conversationId,
type: isGroupChat ? 'group' : 'single',
chatType: isGroupChat ? 1 : 0,
// 对方信息
targetId: this.extractTargetId(conv.conversationId, isGroupChat),
name: conv.name || conv.conversationName || '未知',
avatar: conv.avatar || '',
// 会话状态
isPinned: conv.stickTop || false,
isTop: conv.stickTop || false,
isMuted: conv.mute || false,
unreadCount: conv.unreadCount || 0,
// 🔥 最后消息信息 - 使用消息时间messageRefer.createTime
lastMessage: lastMessageText || '',
lastMessageType: this.getMessageType(lastMessage),
lastMessageTime: this.formatMessageTime(messageTime),
lastMsgTime: messageTime,
messageStatus: this.getMessageStatus(lastMessage),
// 在线状态(单聊)
isOnline: false, // 需要额外获取
// 原始 NIM 数据(用于调试)
_nimData: conv
}
}
/**
* 从会话ID中提取目标用户ID
* @param {String} conversationId - 会话ID
* @param {Boolean} isGroup - 是否是群聊
* @returns {String} 目标ID
*/
extractTargetId(conversationId, isGroup) {
if (!conversationId) return ''
// NIM会话ID格式: p2p-账号 或 team-群ID
if (isGroup) {
return conversationId.replace('team-', '')
} else {
return conversationId.replace('p2p-', '')
}
}
/**
* 获取消息预览文本
* @param {Object} message - 消息对象
* @returns {String} 预览文本
*/
getMessagePreview(message) {
if (!message) return ''
const messageType = message.messageType
// 兼容处理:如果 messageType 不存在,尝试从 text 字段获取文本
if (messageType === undefined || messageType === null) {
return message.text || ''
}
switch (messageType) {
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT:
return message.text || ''
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE:
return '[图片]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO:
return '[语音]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO:
return '[视频]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE:
return '[文件]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION:
return '[位置]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION:
return '[通知]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIP:
return '[提示]'
case V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM:
return '[自定义消息]'
default:
return '[未知消息]'
}
}
/**
* 获取消息类型
* @param {Object} message - 消息对象
* @returns {String} 消息类型
*/
getMessageType(message) {
if (!message || !message.messageType) return 'text'
const typeMap = {
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT]: 'text',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE]: 'image',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO]: 'audio',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO]: 'video',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE]: 'file',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION]: 'location',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION]: 'notification',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIP]: 'tip',
[V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM]: 'custom'
}
return typeMap[message.messageType] || 'text'
}
/**
* 获取消息状态
* @param {Object} message - 消息对象
* @returns {String} 消息状态
*/
getMessageStatus(message) {
if (!message) return 'sent'
if (message.sendingState === V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING) {
return 'sending'
} else if (message.sendingState === V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_FAILED) {
return 'failed'
} else {
return 'sent'
}
}
/**
* 格式化消息时间Telegram风格
* @param {Number} timestamp - 时间戳
* @returns {String} 格式化的时间
*/
formatMessageTime(timestamp) {
if (!timestamp) return ''
const now = new Date()
const msg = new Date(timestamp)
// 今天HH:mm
if (now.toDateString() === msg.toDateString()) {
const hh = msg.getHours().toString().padStart(2, '0')
const mm = msg.getMinutes().toString().padStart(2, '0')
return `${hh}:${mm}`
}
// 昨天
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
if (yesterday.toDateString() === msg.toDateString()) {
return '昨天'
}
// 更早YYYY/MM/DD
const year = msg.getFullYear()
const month = (msg.getMonth() + 1).toString().padStart(2, '0')
const day = msg.getDate().toString().padStart(2, '0')
return `${year}/${month}/${day}`
}
/**
* 处理会话新增
*/
handleConversationCreated(conversation) {
const normalized = this.normalizeConversation(conversation)
if (normalized) {
this.conversations.unshift(normalized)
this.emit('conversation_created', normalized)
}
}
/**
* 处理会话删除
*/
handleConversationDeleted(conversationIds) {
if (!Array.isArray(conversationIds)) return
this.conversations = this.conversations.filter(
conv => !conversationIds.includes(conv.conversationId)
)
this.emit('conversation_deleted', conversationIds)
}
/**
* 处理会话更新
*/
handleConversationChanged(conversations) {
if (!Array.isArray(conversations)) return
conversations.forEach(updatedConv => {
const normalized = this.normalizeConversation(updatedConv)
if (!normalized) return
const index = this.conversations.findIndex(
conv => conv.conversationId === normalized.conversationId
)
if (index >= 0) {
this.conversations[index] = normalized
} else {
this.conversations.unshift(normalized)
}
})
this.emit('conversation_changed', this.conversations)
}
/**
* 处理新消息(更新会话)
*/
handleNewMessages(messages) {
if (!Array.isArray(messages)) {
console.warn('⚠️ handleNewMessages 收到非数组消息:', messages)
return
}
console.log('📨 处理新消息,数量:', messages.length)
let hasUpdates = false
messages.forEach(message => {
const conversationId = message.conversationId
if (!conversationId) {
console.warn('⚠️ 消息缺少 conversationId:', message)
return
}
const index = this.conversations.findIndex(
conv => conv.conversationId === conversationId
)
if (index >= 0) {
// 更新现有会话
const conv = this.conversations[index]
conv.lastMessage = this.getMessagePreview(message)
conv.lastMessageType = this.getMessageType(message)
// 🔥 使用消息的 createTime如果没有则保持原时间
const messageTime = message.createTime || conv.lastMsgTime || 0
conv.lastMessageTime = this.formatMessageTime(messageTime)
conv.lastMsgTime = messageTime
conv.unreadCount = (conv.unreadCount || 0) + 1
// 移到列表顶部
this.conversations.splice(index, 1)
this.conversations.unshift(conv)
hasUpdates = true
console.log('✅ 已更新会话:', conv.name || conversationId, '未读数:', conv.unreadCount)
} else {
console.log('⚠️ 会话不在本地列表中conversationId:', conversationId)
}
})
if (hasUpdates) {
console.log('📤 触发 new_message 事件,通知页面刷新')
}
// 🔥 无论会话是否存在于本地列表,都触发事件让页面处理
// 页面会根据情况决定是更新现有会话还是重新加载会话列表
this.emit('new_message', messages)
}
/**
* 处理消息撤回
*/
handleMessageRevoke(revokeNotifications) {
if (!Array.isArray(revokeNotifications)) return
revokeNotifications.forEach(notification => {
const conversationId = notification.conversationId
if (!conversationId) return
const conv = this.conversations.find(
c => c.conversationId === conversationId
)
if (conv && notification.messageRefer) {
// 如果撤回的是最后一条消息,更新预览
conv.lastMessage = '已撤回'
conv.lastMessageType = 'system'
}
})
this.emit('message_revoked', revokeNotifications)
}
/**
* 置顶/取消置顶会话
* @param {String} conversationId - 会话ID
* @param {Boolean} stickTop - 是否置顶
*/
async setConversationStickTop(conversationId, stickTop) {
if (!this.nim) {
throw new Error('NIM实例未初始化')
}
try {
// 🔥 使用云端会话服务设置置顶
await this.nim.V2NIMConversationService.setConversationStickTop(
conversationId,
stickTop
)
// 更新本地缓存
const conv = this.conversations.find(c => c.conversationId === conversationId)
if (conv) {
conv.isPinned = stickTop
conv.isTop = stickTop
}
console.log(`${stickTop ? '置顶' : '取消置顶'}会话成功:`, conversationId)
return true
} catch (error) {
console.error('❌ 设置会话置顶失败:', error)
throw error
}
}
/**
* 删除会话
* @param {String} conversationId - 会话ID
*/
async deleteConversation(conversationId) {
if (!this.nim) {
throw new Error('NIM实例未初始化')
}
try {
// 🔥 使用云端会话服务删除会话
await this.nim.V2NIMConversationService.deleteConversation(conversationId)
// 从缓存中移除
this.conversations = this.conversations.filter(
conv => conv.conversationId !== conversationId
)
console.log('✅ 删除会话成功:', conversationId)
return true
} catch (error) {
console.error('❌ 删除会话失败:', error)
throw error
}
}
/**
* 清空会话未读数(标记会话已读)
* @param {String} conversationId - 会话ID
*/
async clearUnreadCount(conversationId) {
if (!this.nim) {
throw new Error('NIM实例未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
try {
console.log('📖 开始清空会话未读数 (NIM SDK V2):', conversationId)
// 🔥 NIM SDK V2 使用 clearUnreadCountByIds 方法清除未读数
// 参数是会话ID数组
await this.nim.V2NIMConversationService.clearUnreadCountByIds([conversationId])
// 更新本地缓存
const conv = this.conversations.find(c => c.conversationId === conversationId)
if (conv) {
conv.unreadCount = 0
}
console.log('✅ 清空会话未读数成功:', conversationId)
return true
} catch (error) {
console.error('❌ 清空会话未读数失败:', error)
throw error
}
}
/**
* 标记会话已读(别名方法,方便调用)
* @param {String} conversationId - 会话ID
*/
async markConversationRead(conversationId) {
return await this.clearUnreadCount(conversationId)
}
/**
* 获取总未读数
* @returns {Number} 总未读数
*/
getTotalUnreadCount() {
return this.conversations.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0)
}
/**
* 注册事件监听器
* @param {String} event - 事件名称
* @param {Function} callback - 回调函数
*/
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event).push(callback)
}
/**
* 移除事件监听器
* @param {String} event - 事件名称
* @param {Function} callback - 回调函数
*/
off(event, callback) {
if (!this.listeners.has(event)) return
const callbacks = this.listeners.get(event)
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
/**
* 触发事件
* @param {String} event - 事件名称
* @param {*} data - 事件数据
*/
emit(event, data) {
if (!this.listeners.has(event)) return
const callbacks = this.listeners.get(event)
callbacks.forEach(callback => {
try {
callback(data)
} catch (error) {
console.error(`❌ 执行事件回调失败 [${event}]:`, error)
}
})
}
/**
* 销毁管理器
*/
destroy() {
this.listeners.clear()
this.conversations = []
this.nim = null
this.isInitialized = false
console.log('✅ NIM会话管理器已销毁')
}
// ==================== 🔥 消息发送功能 ====================
/**
* 发送文本消息
* @param {String} conversationId - 会话ID
* @param {String} text - 消息内容
* @param {Object} options - 可选参数
* @returns {Promise<Object>} 发送结果
*/
async sendTextMessage(conversationId, text, options = {}) {
if (!this.nim || !this.nim.V2NIMMessageService) {
throw new Error('NIM消息服务未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
if (!text || !text.trim()) {
throw new Error('消息内容不能为空')
}
try {
console.log('📤 发送文本消息:', { conversationId, text })
// 创建文本消息
const message = this.nim.V2NIMMessageCreator.createTextMessage(text)
// 发送消息
const sendResult = await this.nim.V2NIMMessageService.sendMessage(
message,
conversationId
)
console.log('✅ 文本消息发送成功')
// NIM SDK 返回的是包装对象,真正的消息在 sendResult.message 中
const actualMessage = sendResult.message || sendResult
return {
success: true,
messageId: actualMessage.messageClientId,
serverId: actualMessage.messageServerId,
createTime: actualMessage.createTime,
conversationId: conversationId,
message: actualMessage
}
} catch (error) {
console.error('❌ 发送文本消息失败:', error)
throw error
}
}
/**
* 发送图片消息
* @param {String} conversationId - 会话ID
* @param {String} imagePath - 图片路径本地临时路径或URL
* @param {Object} options - 可选参数
* @returns {Promise<Object>} 发送结果
*/
async sendImageMessage(conversationId, imagePath, options = {}) {
if (!this.nim || !this.nim.V2NIMMessageService) {
throw new Error('NIM消息服务未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
if (!imagePath) {
throw new Error('图片路径不能为空')
}
try {
console.log('📤 发送图片消息:', { conversationId, imagePath })
// 创建图片消息
const message = this.nim.V2NIMMessageCreator.createImageMessage(imagePath)
// 发送消息
const sendResult = await this.nim.V2NIMMessageService.sendMessage(
message,
conversationId
)
console.log('✅ 图片消息发送成功')
const actualMessage = sendResult.message || sendResult
return {
success: true,
conversationId,
messageId: actualMessage.messageClientId,
serverId: actualMessage.messageServerId,
createTime: actualMessage.createTime,
message: actualMessage
}
} catch (error) {
console.error('❌ 发送图片消息失败:', error)
throw error
}
}
/**
* 发送语音消息
* @param {String} conversationId - 会话ID直接使用当前会话ID
* @param {String} audioPath - 语音文件路径(微信小程序临时文件路径)
* @param {Number} duration - 语音时长(毫秒)
* @param {Object} options - 可选参数
* @returns {Promise<Object>} 发送结果
*/
async sendAudioMessage(conversationId, audioPath, duration, options = {}) {
if (!this.nim || !this.nim.V2NIMMessageService) {
throw new Error('NIM消息服务未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
if (!audioPath || typeof audioPath !== 'string' || !audioPath.trim()) {
console.error('❌ 语音文件路径无效:', audioPath);
throw new Error('语音文件路径不能为空')
}
try {
console.log('📤 发送语音消息:', { conversationId, audioPath, duration })
// 🔥 小程序环境下创建语音消息需要使用特殊参数
// 参考: https://doc.yunxin.163.com/messaging2/guide/Dk0MTA5MDI?platform=miniapp
const message = this.nim.V2NIMMessageCreator.createAudioMessage(
audioPath // 微信小程序临时文件路径
)
console.log('✅ 创建语音消息对象成功')
// 发送消息
const sendResult = await this.nim.V2NIMMessageService.sendMessage(
message,
conversationId
)
console.log('✅ 语音消息发送成功')
const actualMessage = sendResult.message || sendResult
return {
success: true,
conversationId,
messageId: actualMessage.messageClientId,
serverId: actualMessage.messageServerId,
createTime: actualMessage.createTime,
message: actualMessage
}
} catch (error) {
console.error('❌ 发送语音消息失败:', error)
throw error
}
}
/**
* 发送视频消息
* @param {String} conversationId - 会话ID直接使用当前会话ID
* @param {String} videoPath - 视频文件路径
* @param {Number} duration - 视频时长(毫秒)
* @param {Object} options - 可选参数
* @returns {Promise<Object>} 发送结果
*/
async sendVideoMessage(conversationId, videoPath, duration, options = {}) {
if (!this.nim || !this.nim.V2NIMMessageService) {
throw new Error('NIM消息服务未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
if (!videoPath) {
throw new Error('视频文件路径不能为空')
}
try {
console.log('📤 发送视频消息:', { conversationId, duration })
// 创建视频消息
const message = this.nim.V2NIMMessageCreator.createVideoMessage(videoPath)
// 发送消息
const sendResult = await this.nim.V2NIMMessageService.sendMessage(
message,
conversationId
)
console.log('✅ 视频消息发送成功')
const actualMessage = sendResult.message || sendResult
return {
success: true,
conversationId,
messageId: actualMessage.messageClientId,
serverId: actualMessage.messageServerId,
createTime: actualMessage.createTime,
message: actualMessage
}
} catch (error) {
console.error('❌ 发送视频消息失败:', error)
throw error
}
}
/**
* 发送文件消息
* @param {String} conversationId - 会话ID直接使用当前会话ID
* @param {String} filePath - 文件路径
* @param {Object} options - 可选参数
* @returns {Promise<Object>} 发送结果
*/
async sendFileMessage(conversationId, filePath, options = {}) {
if (!this.nim || !this.nim.V2NIMMessageService) {
throw new Error('NIM消息服务未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
if (!filePath) {
throw new Error('文件路径不能为空')
}
try {
console.log('📤 发送文件消息:', { conversationId })
// 创建文件消息
const message = this.nim.V2NIMMessageCreator.createFileMessage(filePath)
// 发送消息
const sendResult = await this.nim.V2NIMMessageService.sendMessage(
message,
conversationId
)
console.log('✅ 文件消息发送成功')
const actualMessage = sendResult.message || sendResult
return {
success: true,
conversationId,
messageId: actualMessage.messageClientId,
serverId: actualMessage.messageServerId,
createTime: actualMessage.createTime,
message: actualMessage
}
} catch (error) {
console.error('❌ 发送文件消息失败:', error)
throw error
}
}
/**
* 发送位置消息
* @param {String} conversationId - 会话ID直接使用当前会话ID
* @param {Number} latitude - 纬度
* @param {Number} longitude - 经度
* @param {String} address - 地址描述
* @param {Object} options - 可选参数
* @returns {Promise<Object>} 发送结果
*/
async sendLocationMessage(conversationId, latitude, longitude, address, options = {}) {
if (!this.nim || !this.nim.V2NIMMessageService) {
throw new Error('NIM消息服务未初始化')
}
if (!conversationId || !conversationId.trim()) {
throw new Error('会话ID不能为空')
}
if (typeof latitude !== 'number' || typeof longitude !== 'number') {
throw new Error('经纬度必须是数字')
}
try {
console.log('📤 发送位置消息:', { conversationId, latitude, longitude })
// 创建位置消息
const message = this.nim.V2NIMMessageCreator.createLocationMessage(latitude, longitude, address || '')
// 发送消息
const sendResult = await this.nim.V2NIMMessageService.sendMessage(
message,
conversationId
)
console.log('✅ 位置消息发送成功')
const actualMessage = sendResult.message || sendResult
return {
success: true,
conversationId,
messageId: actualMessage.messageClientId,
serverId: actualMessage.messageServerId,
createTime: actualMessage.createTime,
message: actualMessage
}
} catch (error) {
console.error('❌ 发送位置消息失败:', error)
throw error
}
}
/**
* 撤回消息
* @param {String} conversationId - 会话ID
* @param {Object} message - 消息对象(必须包含完整的消息信息)
* @returns {Promise<Object>} 撤回结果
*/
async recallMessage(conversationId, message) {
if (!this.nim) {
throw new Error('NIM实例未初始化')
}
try {
console.log('↩️ 撤回消息:', { conversationId, messageId: message?.messageServerId || message?.messageClientId })
// 🔥 使用 NIM SDK V2 的 revokeMessage 方法
// 根据官方文档revokeMessage 需要传入完整的消息对象和撤回参数
// 参数1: message - 完整的消息对象
// 参数2: revokeParams - V2NIMMessageRevokeParams 对象(可选)
const revokeResult = await this.nim.V2NIMMessageService.revokeMessage(
message,
{
serverExtension: '', // 服务端扩展字段(可选)
postscript: '' // 撤回附言(可选)
}
)
console.log('✅ 消息撤回成功:', revokeResult)
return {
success: true,
conversationId,
messageId: message.messageServerId || message.messageClientId,
revokeTime: Date.now()
}
} catch (error) {
console.error('❌ 撤回消息失败:', error)
throw error
}
}
/**
* 检查管理器是否已初始化
* @returns {Boolean} 是否已初始化
*/
getInitStatus() {
return this.isInitialized && this.nim !== null
}
}
// 导出单例
const nimConversationManager = new NIMConversationManager()
module.exports = nimConversationManager