987 lines
28 KiB
JavaScript
987 lines
28 KiB
JavaScript
/**
|
||
* 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
|