import store from '@/config/store'; import base from '@/config/baseUrl'; class MattermostClient { constructor() { // 基础属性 this.ready = false; this.userBaseUrl = "https://im.sjqqzc.top"; // 用户 API this.adminBaseUrl = "https://q.sjqqzc.top"; // 管理员 API this.ws = null; this.wsUrl = ''; this.userToken = ''; // 用户令牌 this.adminToken = ''; // 管理员令牌 this.connected = false; this.authenticated = false; this.sequence = 1; // 重连相关 this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectInterval = 3000; this.reconnectTimer = null; // 心跳相关 this.heartbeatInterval = 30000; // 30秒 this.heartbeatTimer = null; this.lastHeartbeatResponse = Date.now(); // 消息处理 this.messageHandlers = new Map(); // 修改为 Map 类型,key 为事件名,value 为 Set 类型的处理器集合 this.pendingMessages = new Map(); // 存储等待响应的消息 this.messageTimeoutEnabled = false; // 消息响应超时开关,默认关闭 this.messageTimeout = 10000; // 消息响应超时时间(毫秒) // 状态回调 this.statusChangeCallbacks = new Set(); // 频道相关 this.channels = new Map(); // 存储频道信息 this.directChannels = new Map(); // 存储私聊频道 this.unreadCounts = new Map(); // 存储未读消息数 // 用户相关 this.currentUser = null; this.userStatuses = new Map(); // 存储用户在线状态 this.userRoles = new Map(); // 存储用户角色 // 消息相关 this.messageCache = new Map(); // 缓存消息 this.deletedMessages = new Set(); // 记录已删除的消息 this.kown_users={}; // 存储已知的用户信息 } // 初始化连接 async init() { if(this.ready){ return; } if (!store.state.userInfo || !store.state.userInfo.im_token || !store.state.userInfo.token) { console.log('用户令牌是必需的'); return ; } this.userToken = store.state.userInfo.im_token; this.adminToken = store.state.userInfo.token; this.currentUser = store.state.userInfo.im_user; this.kown_users[this.currentUser.id] = this.currentUser; this.getCurrentUser(); this.ready = true; // 根据环境选择 ws 或 wss const protocol = process.env.NODE_ENV === 'development' ? 'ws' : 'wss'; // 构建 WebSocket URL,使用用户 API 域名 const connectionId = Date.now().toString(36) + Math.random().toString(36).substr(2); this.wsUrl = `${protocol}://im.sjqqzc.top/api/v4/websocket?connection_id=${connectionId}&sequence_number=0`; // 重置状态 this.sequence = 1; this.reconnectAttempts = 0; this.authenticated = false; // 建立新连接 await this.connect(); // 启动心跳检测 this.startHeartbeat(); } // 建立 WebSocket 连接 connect() { return new Promise((resolve, reject) => { try { // 清理可能存在的旧连接 if (this.ws) { return ; //this.ws.close(); //this.ws = null; } this.ws = uni.connectSocket({ url: this.wsUrl, header: { 'Authorization': `Bearer ${this.userToken}` }, multiple: true, success: () => { console.log('WebSocket 连接创建成功'); }, fail: (error) => { console.error('WebSocket 连接创建失败:', error); this.handleConnectionError(error); reject(error); } }); this.ws.onOpen(() => { console.log('WebSocket 已打开,准备认证'); this.connected = true; this.notifyStatusChange('connected'); // 发送认证信息,使用正确的认证消息格式 const authMessage = { action: 'authentication_challenge', seq: this.sequence, data: { token: this.userToken } }; this.sequence++; this.ws.send({ data: JSON.stringify(authMessage), success: () => { console.log('认证信息发送成功:', authMessage); // 注意:这里不立即设置 authenticated 为 true // 等待服务器的认证响应 }, fail: (error) => { console.error('认证信息发送失败:', error); this.handleConnectionError(error); reject(error); } }); }); this.ws.onClose(() => { console.log('WebSocket 已关闭'); this.handleConnectionClose(); }); this.ws.onError((error) => { console.error('WebSocket 错误:', error); this.handleConnectionError(error); }); this.ws.onMessage((res) => { try { const message = JSON.parse(res.data); console.log('收到消息:', message); // 处理认证响应 if (message.event === 'hello') { console.log('认证成功'); this.authenticated = true; this.notifyStatusChange('authenticated'); this.reconnectAttempts = 0; resolve(); return; } // 处理认证失败 if (message.event === 'authentication_challenge' && message.data && message.data.status !== 'OK') { console.error('认证失败:', message); this.handleConnectionError(new Error('认证失败')); reject(new Error('认证失败')); return; } this.handleMessage(message); } catch (error) { console.error('消息解析错误:', error); } }); } catch (error) { console.error('创建 WebSocket 连接失败:', error); this.handleConnectionError(error); reject(error); } }); } // 处理连接错误 handleConnectionError(error) { this.connected = false; this.authenticated = false; this.notifyStatusChange('error', error); this.handleReconnect(); } // 处理连接关闭 handleConnectionClose() { this.connected = false; this.authenticated = false; this.notifyStatusChange('disconnected'); this.stopHeartbeat(); this.handleReconnect(); } // 处理重连 handleReconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); this.notifyStatusChange('reconnecting', this.reconnectAttempts); this.reconnectTimer = setTimeout(() => { this.connect().catch(error => { console.error('重连失败:', error); }); }, this.reconnectInterval); } else { console.error('达到最大重连次数,停止重连'); this.notifyStatusChange('failed'); } } // 启动心跳检测 startHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); } this.heartbeatTimer = setInterval(() => { if (this.connected && this.authenticated) { const now = Date.now(); if (now - this.lastHeartbeatResponse > this.heartbeatInterval * 2) { console.log('心跳超时,准备重连'); this.handleConnectionClose(); return; } // 发送 ping 消息 this.sendWebSocketMessage('ping').catch(error => { console.error('心跳消息发送失败:', error); }); } }, this.heartbeatInterval); } // 停止心跳检测 stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } // 设置消息响应超时开关 setMessageTimeoutEnabled(enabled) { this.messageTimeoutEnabled = enabled; console.log(`消息响应超时已${enabled ? '启用' : '禁用'}`); } // 设置消息响应超时时间 setMessageTimeout(timeout) { if (timeout < 1000) { console.warn('消息响应超时时间不能小于1000毫秒'); return; } this.messageTimeout = timeout; console.log(`消息响应超时时间已设置为${timeout}毫秒`); } // 获取消息响应超时设置 getMessageTimeoutSettings() { return { enabled: this.messageTimeoutEnabled, timeout: this.messageTimeout }; } // 发送 WebSocket 消息 sendWebSocketMessage(action, data = {}) { if (!this.connected) { return Promise.reject(new Error('WebSocket 未连接')); } // 构建符合 Mattermost 格式的消息 const message = { action, seq: this.sequence }; // 只有在非 ping 消息时才添加其他数据 if (action !== 'ping' && Object.keys(data).length > 0) { Object.assign(message, data); } // 增加序列号 this.sequence++; return new Promise((resolve, reject) => { // 存储等待响应的消息 this.pendingMessages.set(message.seq, { resolve, reject }); // 设置超时处理 let timeout; if (this.messageTimeoutEnabled) { timeout = setTimeout(() => { if (this.pendingMessages.has(message.seq)) { this.pendingMessages.delete(message.seq); reject(new Error(`消息 ${message.seq} 响应超时`)); } }, this.messageTimeout); } this.ws.send({ data: JSON.stringify(message), success: () => { console.log('WebSocket 消息发送成功:', message); }, fail: (error) => { console.error('WebSocket 消息发送失败:', error); this.pendingMessages.delete(message.seq); if (timeout) { clearTimeout(timeout); } reject(error); } }); }); } // 处理接收到的消息 handleMessage(message) { console.log('处理消息:', message); // 更新心跳时间戳 if (message.action === 'pong') { this.lastHeartbeatResponse = Date.now(); return; } // 处理消息响应 // if (message.seq && this.pendingMessages.has(message.seq)) { // const { resolve } = this.pendingMessages.get(message.seq); // this.pendingMessages.delete(message.seq); // resolve(message); // return; // } // 更新序列号 if (message.seq) { this.sequence = message.seq + 1; } // 处理不同类型的消息 if (message.event === 'posted') { const post = message.data; // 更新未读消息数 if (post.channel_id && !this.isCurrentChannel(post.channel_id)) { const currentCount = this.unreadCounts.get(post.channel_id) || 0; this.unreadCounts.set(post.channel_id, currentCount + 1); } // 缓存消息 this.messageCache.set(post.id, post); // 如果当前不在聊天页面,增加未读数 const currentPage = getCurrentPages(); const isInChatPage = currentPage.some(page => page.route === 'pages/im/chat' && ((page.$page.options.target_id == this.getCurrentUserId() && page.$page.options.target_type === 'private') || (page.$page.options.target_id == '3awtqn6b17d4bp8ro68qmzo96o' && page.$page.options.target_type === 'group')) ); if(!isInChatPage){ uni.createPushMessage({ title:post.channel_display_name.substr(0,1).replace("_","@"), content:post.message, payload:post, //icon:'', //sound:'', cover:false, complete(){ } }); } } else if (message.event === 'post_deleted') { // 处理消息删除 const postId = message.data.post_id; this.deletedMessages.add(postId); this.messageCache.delete(postId); } else if (message.event === 'user_updated') { // 处理用户信息更新 const user = message.data; if (user.roles) { this.userRoles.set(user.id, user.roles); } } else if (message.event === 'status_change') { // 处理用户状态变化 const status = message.data; this.userStatuses.set(status.user_id, status); } // 触发消息处理器 if (message.event && this.messageHandlers.has(message.event)) { console.log(`触发消息处理器: ${message.event}, 处理器数量: ${this.messageHandlers.get(message.event).size}`); this.messageHandlers.get(message.event).forEach(handler => { try { handler(message.data); } catch (error) { console.error('消息处理器错误:', error); } }); } } // 注册状态变化回调 onStatusChange(callback) { this.statusChangeCallbacks.add(callback); } // 移除状态变化回调 offStatusChange(callback) { this.statusChangeCallbacks.delete(callback); } // 通知状态变化 notifyStatusChange(status, data = null) { this.statusChangeCallbacks.forEach(callback => { try { callback(status, data); } catch (error) { console.error('状态变化回调错误:', error); } }); } // 注册消息处理器 on(event, handler) { if (!this.messageHandlers.has(event)) { this.messageHandlers.set(event, new Set()); } this.messageHandlers.get(event).add(handler); console.log(`注册消息处理器: ${event}, 当前处理器数量: ${this.messageHandlers.get(event).size}`); } // 移除消息处理器 off(event, handler) { if (this.messageHandlers.has(event)) { this.messageHandlers.get(event).delete(handler); console.log(`移除消息处理器: ${event}, 剩余处理器数量: ${this.messageHandlers.get(event).size}`); } } // 获取连接状态 getConnectionStatus() { return { connected: this.connected, authenticated: this.authenticated, reconnectAttempts: this.reconnectAttempts, maxReconnectAttempts: this.maxReconnectAttempts, wsUrl: this.wsUrl, lastHeartbeat: this.lastHeartbeatResponse }; } // 断开连接 async disconnect() { this.stopHeartbeat(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.connected = false; this.authenticated = false; this.notifyStatusChange('disconnected'); // 清理所有等待响应的消息 this.pendingMessages.forEach(({ reject }) => { reject(new Error('连接已断开')); }); this.pendingMessages.clear(); } async getRecentContacts(){ const channels = await this.request({ url: `/api/v4/users/me/channels`, method: 'GET' }); let result = {}; let unkown_user_ids = []; if (channels && channels.length > 0) { const dmChannels = channels .filter(c => c.type === 'D') .sort((a, b) => b.last_post_at - a.last_post_at); // 按最后消息时间降序 dmChannels.forEach(channel => { var member_ids = channel.name.replace(this.currentUser.id,'').replace('__',''); if(!this.kown_users[member_ids]){ result[member_ids]={}; unkown_user_ids.push(member_ids); }else{ result[member_ids]=this.kown_users[member_ids]; } }); if(unkown_user_ids.length>0){ const users = await this.getUsersByIds(unkown_user_ids); users.data.forEach(user => { this.kown_users[user.id]=user; result[user.id]=user; }); } return result; } return []; } // 获取私聊频道 async getDirectChannels() { //return []; try { const channels = await this.request({ url: `/api/v4/users/me/channels`, method: 'GET' }); console.log(channels); if (channels && channels.length > 0) { channels.forEach(channel => { this.directChannels.set(channel.id, channel); }); return channels; } return []; } catch (error) { console.error('获取私聊频道错误:', error); throw error; } } // HTTP 请求封装 async request(options) { let { url, method = 'GET', data = {}, headers = {},to_server='user' } = options; let requestHeaders ={}; if(to_server=='user'){ // 添加认证头 requestHeaders = { 'Authorization': `Bearer ${this.userToken}`, ...headers }; url = this.userBaseUrl + url; }else{ // 添加认证头 requestHeaders = { 'token': this.adminToken, ...headers }; url = this.adminBaseUrl + url; } const [error, response] = await uni.request({ url, method, data, header: requestHeaders }).catch(err => [err, null]); if (error) { console.error(`请求失败 [${method} ${url}]:`, error); throw error; } if (response.statusCode >= 200 && response.statusCode < 300) { return response.data; } console.error(`请求失败 [${method} ${url}]:`, response); throw new Error(`请求失败: ${response.statusCode}`); } // 文件上传封装 async uploadFile(options) { const { url, filePath, name = 'files', headers = {} } = options; // 添加认证头 const requestHeaders = { 'Authorization': `Bearer ${this.userToken}`, ...headers }; const [error, response] = await uni.uploadFile({ url, filePath, name, header: requestHeaders }).catch(err => [err, null]); if (error) { console.error(`文件上传失败 [${url}]:`, error); throw error; } if (response.statusCode >= 200 && response.statusCode < 300) { return response.data; } console.error(`文件上传失败 [${url}]:`, response); throw new Error(`文件上传失败: ${response.statusCode}`); } // 获取频道消息 async getChannelPosts(channelId, page = 0, perPage = 30) { try { const response = await this.request({ url: `/api/v4/channels/${channelId}/posts`, method: 'GET', data: { page, per_page: perPage, include_deleted: false } }); if (!response || !response.order || !response.posts) { console.error('无效的消息数据格式:', response); return []; } return response.order.reverse().map(id => response.posts[id]); } catch (error) { console.error('获取消息失败:', error); return []; } } // 发送文本消息 async sendTextMessage(channelId, message) { return this.request({ url: `/api/v4/posts`, method: 'POST', data: { channel_id: channelId, message: message, root_id: '', file_ids: [], props: {} } }); } // 发送图片消息 async sendImageMessage(channelId, filePath) { try { // 1. 上传文件 const fileData = await this.uploadFile({ url: `${this.userBaseUrl}/api/v4/files`, filePath }); const fileInfo = JSON.parse(fileData); const fileId = fileInfo.file_infos[0].id; // 2. 发送带图片的消息 return this.request({ url: `${this.userBaseUrl}/api/v4/posts`, method: 'POST', data: { channel_id: channelId, message: '', file_ids: [fileId], props: {} } }); } catch (error) { console.error('发送图片消息失败:', error); throw error; } } // 标记消息为已读 async markChannelAsViewed(channelId) { if (!this.authenticated) { throw new Error('未认证'); } const response = await this.request({ url: `/api/v4/channels/${channelId}/view`, method: 'POST', data: { channel_id: channelId } }); this.unreadCounts.set(channelId, 0); return response; } // 检查是否是当前查看的频道 isCurrentChannel(channelId) { return this.currentChannelId === channelId; } // 获取频道的未读消息数 getUnreadCount(channelId) { return this.unreadCounts.get(channelId) || 0; } // 设置当前查看的频道 setCurrentChannel(channelId) { this.currentChannelId = channelId; // 自动标记为已读 if (channelId) { this.markChannelAsViewed(channelId).catch(console.error); } } // 获取当前用户信息 async getCurrentUser() { // if (!this.authenticated) { // throw new Error('未认证'); // } const user = await this.request({ url: `/api/v4/users/me`, method: 'GET' }); this.currentUser = user; this.userRoles.set(user.id, user.roles); return user; } // 检查用户是否是管理员 isAdmin(userId) { const roles = this.userRoles.get(userId); return roles && roles.includes('system_admin'); } // 检查用户是否是客服 isKefu(userId) { const roles = this.userRoles.get(userId); return roles && roles.includes('system_user'); } // 获取用户状态 async getUserStatus(userId) { if (!this.authenticated) { throw new Error('未认证'); } try { // 使用用户 API 获取状态 const response = await this.request({ to_server:'admin', url: `/api/v4/users/${userId}/status`, method: 'GET' }); if (response.statusCode === 200) { this.userStatuses.set(userId, response.data); return response.data; } throw new Error('获取用户状态失败'); } catch (error) { console.error('获取用户状态错误:', error); throw error; } } // 获取多个用户状态 async getUsersStatus(userIds) { if (!this.authenticated) { throw new Error('未认证'); } try { const response = await this.request({ to_server:'admin', url: `/api/v4/users/status/ids`, method: 'POST', data: userIds }); if (response.statusCode === 200) { response.data.forEach(status => { this.userStatuses.set(status.user_id, status); }); return response.data; } throw new Error('获取用户状态失败'); } catch (error) { console.error('获取用户状态错误:', error); throw error; } } // 搜索消息 async searchPosts(terms, isOrSearch = false) { if (!this.authenticated) { throw new Error('未认证'); } try { const response = await this.request({ url: `/api/v4/posts/search`, method: 'POST', data: { terms, is_or_search: isOrSearch } }); if (response.statusCode === 200) { return response.data; } throw new Error('搜索消息失败'); } catch (error) { console.error('搜索消息错误:', error); throw error; } } // 删除消息(管理员功能) async deletePost(postId) { if (!this.authenticated) { throw new Error('未认证'); } if (!this.isAdmin(this.currentUser?.id)) { throw new Error('没有权限删除消息'); } try { const response = await this.request({ to_server:'admin', url: `/api/v4/posts/${postId}`, method: 'DELETE', }); if (response.statusCode === 200) { this.deletedMessages.add(postId); return response.data; } throw new Error('删除消息失败'); } catch (error) { console.error('删除消息错误:', error); throw error; } } // 撤回消息 async editPost(postId, message) { if (!this.authenticated) { throw new Error('未认证'); } try { const response = await this.request({ url: `/api/v4/posts/${postId}`, method: 'PUT', data: { message, has_reactions: false } }); if (response.statusCode === 200) { return response.data; } throw new Error('撤回消息失败'); } catch (error) { console.error('撤回消息错误:', error); throw error; } } // 禁言用户(管理员功能) async muteUser(userId, channelId) { if (!this.authenticated) { throw new Error('未认证'); } if (!this.isAdmin(this.currentUser?.id)) { throw new Error('没有权限禁言用户'); } try { const response = await this.request({ to_server:'admin', url: `/api/v4/channels/${channelId}/members/${userId}/roles`, method: 'PUT', data: { roles: 'channel_user channel_muted' } }); if (response.statusCode === 200) { return response.data; } throw new Error('禁言用户失败'); } catch (error) { console.error('禁言用户错误:', error); throw error; } } // 解除禁言(管理员功能) async unmuteUser(userId, channelId) { if (!this.authenticated) { throw new Error('未认证'); } if (!this.isAdmin(this.currentUser?.id)) { throw new Error('没有权限解除禁言'); } try { const response = await this.request({ to_server:'admin', url: `/api/v4/channels/${channelId}/members/${userId}/roles`, method: 'PUT', data: { roles: 'channel_user' } }); if (response.statusCode === 200) { return response.data; } throw new Error('解除禁言失败'); } catch (error) { console.error('解除禁言错误:', error); throw error; } } // 获取被禁言用户列表(管理员功能) async getMutedUsers(channelId) { if (!this.authenticated) { throw new Error('未认证'); } if (!this.isAdmin(this.currentUser?.id)) { throw new Error('没有权限查看禁言列表'); } try { const response = await this.request({ to_server:'admin', url: `/api/v4/channels/${channelId}/members`, method: 'GET' }); if (response.statusCode === 200) { return response.data.filter(member => member.roles.includes('channel_muted') ); } throw new Error('获取禁言列表失败'); } catch (error) { console.error('获取禁言列表错误:', error); throw error; } } // 检查消息是否被删除 isMessageDeleted(postId) { return this.deletedMessages.has(postId); } // 获取缓存的消息 getCachedMessage(postId) { return this.messageCache.get(postId); } // 清理消息缓存 clearMessageCache() { this.messageCache.clear(); this.deletedMessages.clear(); } // 获取当前用户ID getCurrentUserId() { return this.currentUser?.id; } // 检查是否已认证 isAuthenticated() { return this.authenticated; } async getUserInfo(username){ if(username.indexOf('@')!==-1){ return await this.request({ to_server:'admin', url: `/api/v4/email/${username}`, method: 'GET' }); }else{ return await this.request({ to_server:'admin', url: `/api/v4/users/${username}`, method: 'GET' }); } } async getUsersByIds(user_ids){ return await this.request({ to_server:'admin', url: `/api/v4/users/ids`, method: 'POST', data:JSON.stringify(user_ids) }); } async getChannelById(channel_id){ return await this.request({ url:`/api/v4/channels/${channel_id}`, method: 'GET' }) } async getChannelMembers(channel_id){ return await this.request({ url:`/api/v4/channels/${channel_id}/members`, method: 'GET' }) } async talk_to_user(item){ if (!item.type || item.type!== 'O') { // 获取当前用户ID const currentUserId = await this.getCurrentUserId(); // 获取目标用户ID const targetUserId = item.id; // 查询直接消息频道 const response = await this.request({ url: `/api/v4/channels/direct`, method: 'POST', data: [currentUserId, targetUserId] }); if (response.id) { // 使用返回的频道ID进行跳转 return `/pages/im/chat?target_id=${response.id}&target_type=private`; } } // 如果不是私聊或查询失败,使用原始ID跳转 return `/pages/im/chat?target_id=${item.id}&target_type=group`; } } // 创建单例 const mattermost = new MattermostClient(); export default mattermost;