import axios from 'axios'; import { appConfig } from '@/config'; const LOCAL_MESSAGE_STORAGE_KEY = 'qmt-web-im-local-messages-v2'; const LOCAL_HIDDEN_MESSAGE_STORAGE_KEY = 'qmt-web-im-hidden-messages-v1'; const LOCAL_RECALLED_MESSAGE_STORAGE_KEY = 'qmt-web-im-recalled-messages-v1'; function scopedStorageKey(baseKey, userId = '') { const scope = `${appConfig.appServer || ''}:${userId || 'anonymous'}`; return `${baseKey}:${encodeURIComponent(scope)}`; } function randomId() { if (window.crypto && window.crypto.randomUUID) { return window.crypto.randomUUID().replaceAll('-', ''); } return `${Date.now()}${Math.random().toString(16).slice(2)}`; } function readLocalMessages(userId) { try { return JSON.parse(localStorage.getItem(scopedStorageKey(LOCAL_MESSAGE_STORAGE_KEY, userId)) || '{}'); } catch (error) { return {}; } } function readMessageSet(storageKey, userId) { try { return JSON.parse(localStorage.getItem(scopedStorageKey(storageKey, userId)) || '{}'); } catch (error) { return {}; } } function writeMessageSet(storageKey, messagesByConversation, userId) { localStorage.setItem(scopedStorageKey(storageKey, userId), JSON.stringify(messagesByConversation)); } function messageKeys(message) { return [message?.id, message?.messageUid] .filter(Boolean) .map(value => String(value)); } function hasStoredMessage(storageKey, conversationId, message, userId) { const stored = readMessageSet(storageKey, userId); const ids = new Set(stored[conversationId] || []); return messageKeys(message).some(id => ids.has(id)); } function storeMessageKeys(storageKey, conversationId, messages, userId) { const stored = readMessageSet(storageKey, userId); const ids = new Set(stored[conversationId] || []); messages.forEach(message => { messageKeys(message).forEach(id => ids.add(id)); }); stored[conversationId] = Array.from(ids); writeMessageSet(storageKey, stored, userId); } function withRecalledState(message) { return { ...message, status: 'recalled', deliveryStatus: '', recalled: true, recallStatus: '已撤回' }; } function applyLocalMessageState(conversationId, messages, userId) { return messages .filter(message => !hasStoredMessage(LOCAL_HIDDEN_MESSAGE_STORAGE_KEY, conversationId, message, userId)) .map(message => { const isRecalled = Boolean(message.recalled) || message.status === 'recalled' || hasStoredMessage(LOCAL_RECALLED_MESSAGE_STORAGE_KEY, conversationId, message, userId); if (!isRecalled) { return message; } return withRecalledState(message); }); } function writeLocalMessages(messagesByConversation, userId) { localStorage.setItem(scopedStorageKey(LOCAL_MESSAGE_STORAGE_KEY, userId), JSON.stringify(messagesByConversation)); } function saveLocalMessage(message, userId) { const messagesByConversation = readLocalMessages(userId); messagesByConversation[message.conversationId] = messagesByConversation[message.conversationId] || []; const index = messagesByConversation[message.conversationId].findIndex(item => item.id === message.id); if (index >= 0) { messagesByConversation[message.conversationId].splice(index, 1, message); } else { messagesByConversation[message.conversationId].push(message); } writeLocalMessages(messagesByConversation, userId); } function mergeSyncedMessages(localMessages, remoteMessages, conversationId, userId) { const visibleLocalMessages = applyLocalMessageState(conversationId, localMessages, userId); const localByKey = new Map(); visibleLocalMessages.forEach(message => { messageKeys(message).forEach(key => { if (!localByKey.has(key)) { localByKey.set(key, message); } }); }); const visibleRemoteMessages = applyLocalMessageState(conversationId, remoteMessages, userId).map(remoteMessage => { if (!remoteMessage.recalled || String(remoteMessage.text || '').trim()) { return remoteMessage; } const localMessage = messageKeys(remoteMessage) .map(key => localByKey.get(key)) .find(message => message?.text && message.text !== '你撤回了一条消息'); return localMessage ? { ...remoteMessage, text: localMessage.text } : remoteMessage; }); const remoteKeys = new Set(); visibleRemoteMessages.forEach(message => { messageKeys(message).forEach(key => remoteKeys.add(key)); }); const now = Date.now(); function hasRemoteMatch(localMessage) { if (localMessage.messageUid && remoteKeys.has(localMessage.messageUid)) { return true; } return visibleRemoteMessages.some(remoteMessage => { return remoteMessage.direction === localMessage.direction && remoteMessage.text === localMessage.text && Math.abs(Number(remoteMessage.timestamp || 0) - Number(localMessage.timestamp || 0)) < 15000; }); } const pendingLocalMessages = visibleLocalMessages.filter(message => { if (message.status === 'recalled' || message.recalled) { return message.direction === 'out' && !hasRemoteMatch(message); } if (message.direction !== 'out') { return false; } if (message.status === 'sent' && message.messageUid) { return !hasRemoteMatch(message); } if (message.status === 'sent') { return !hasRemoteMatch(message) && now - Number(message.timestamp || 0) < 30000; } return message.status === 'sending' || message.status === 'failed'; }); return [...visibleRemoteMessages, ...pendingLocalMessages].sort((a, b) => { return Number(a.displayTimestamp || a.timestamp || 0) - Number(b.displayTimestamp || b.timestamp || 0); }); } function getTitle(value, fallback) { return value || fallback || ''; } function getUniqueEquipment(mobile, fallback) { const allowedDevices = { '18301902119': '67072d37062c4f31b1165073552933ba', '18301902116': '1777479418113eeea8e236edce', '18301902118': 'de89fb2f2a61480a8c381f6c16e28bd8', '18301902117': 'd117f480faa64145b6162107b1b49617' }; return allowedDevices[mobile] || localStorage.getItem('uniqueEquipment') || fallback; } function canUseLocalLoginFallback(response) { const message = response.data?.message || ''; return process.env.NODE_ENV === 'development' && message.includes('允许此设备登录'); } export class HttpImClient { constructor() { this.listeners = new Map(); this.connected = false; this.currentUser = null; } getClientId() { let clientId = localStorage.getItem(appConfig.clientIdStorageKey); if (!clientId) { clientId = randomId(); localStorage.setItem(appConfig.clientIdStorageKey, clientId); } return clientId; } restoreClientId(clientId) { if (clientId) { localStorage.setItem(appConfig.clientIdStorageKey, clientId); } } getSdkVersion() { return 'HTTP'; } async ensureSdk() { return true; } async loginWithPassword(mobile, password) { const url = `${appConfig.appServer}/login_pwd`; const deviceId = getUniqueEquipment(mobile, this.getClientId()); const response = await axios.post(url, { mobile, password, platform: appConfig.platform, clientId: deviceId, uniqueEquipment: deviceId }, { withCredentials: false }); this.persistAppToken(url, response); if (response.data?.code !== 0) { if (canUseLocalLoginFallback(response)) { return { userId: mobile, token: `local-device-pending-${Date.now()}`, displayName: mobile, localOnly: true }; } throw new Error(response.data?.message || '登录失败'); } return response.data.result; } async requestAuthCode(mobile) { const response = await axios.post(`${appConfig.appServer}/send_code`, { mobile }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '验证码发送失败'); } return response.data.result; } async loginWithCode(mobile, code) { const url = `${appConfig.appServer}/login`; const deviceId = getUniqueEquipment(mobile, this.getClientId()); const response = await axios.post(url, { mobile, code, platform: appConfig.platform, clientId: deviceId, uniqueEquipment: deviceId }, { withCredentials: false }); this.persistAppToken(url, response); if (response.data?.code !== 0) { throw new Error(response.data?.message || '登录失败'); } return response.data.result; } async registerWithPhone(mobile, code) { const url = `${appConfig.appServer}/register`; const deviceId = getUniqueEquipment(mobile, this.getClientId()); try { const response = await axios.post(url, { mobile, code, platform: appConfig.platform, clientId: deviceId, uniqueEquipment: deviceId }, { withCredentials: false }); this.persistAppToken(url, response); if (response.data?.code !== 0) { throw new Error(response.data?.message || '注册失败'); } return response.data.result; } catch (error) { const status = error.response?.status; if (status === 404 || status === 405) { return this.loginWithCode(mobile, code); } throw error; } } connect(userId, token) { this.currentUser = { userId, token }; this.connected = true; this.emit('connectionStatusChanged', { connected: true, statusText: '已连接' }); this.refreshConversationList(); } disconnect() { this.connected = false; this.currentUser = null; this.emit('connectionStatusChanged', { connected: false, statusText: '未连接' }); } getConversationList() { return []; } refreshConversationList() { const conversations = this.getConversationList(); if (conversations.length) { this.emit('conversationsChanged', conversations); } if (this.currentUser?.userId) { this.fetchRemoteConversations().then(remoteConversations => { this.emit('conversationsChanged', remoteConversations); }).catch(() => {}); } return conversations; } async fetchRemoteConversations(limit = 50) { if (!this.currentUser?.userId) { return []; } const response = await axios.post(appConfig.messagesApi, { userId: this.currentUser.userId, limit }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '会话同步失败'); } return (response.data?.result?.conversations || []).map(conversation => ({ ...conversation, updatedAt: Number(conversation.updatedAt || Date.now()), avatarText: conversation.avatarText || String(conversation.title || conversation.target || '?').slice(0, 1).toUpperCase(), raw: conversation.raw || { type: 'single', target: conversation.target, line: 0 } })); } getMessages(conversation) { if (!conversation?.id) { return []; } const userId = this.currentUser?.userId || ''; return applyLocalMessageState(conversation.id, readLocalMessages(userId)[conversation.id] || [], userId); } async fetchRemoteMessages(conversation, limit = 50, options = {}) { if (!conversation?.id || !this.currentUser?.userId) { return []; } const response = await axios.post(appConfig.messagesApi, { userId: this.currentUser.userId, target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''), limit, conversationType: conversation.type || conversation.raw?.type || 0, markRead: Boolean(options.markRead) }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '消息同步失败'); } const messages = (response.data?.result?.messages || []).map(message => ({ ...message, displayTimestamp: message.displayTimestamp || message.timestamp, conversationId: conversation.id })); const userId = this.currentUser?.userId || ''; const messagesByConversation = readLocalMessages(userId); messagesByConversation[conversation.id] = mergeSyncedMessages(messagesByConversation[conversation.id] || [], messages, conversation.id, userId); writeLocalMessages(messagesByConversation, userId); return messagesByConversation[conversation.id]; } hideMessages(conversation, messages) { const items = Array.isArray(messages) ? messages : [messages]; if (!conversation?.id || !items.length) { return; } const userId = this.currentUser?.userId || ''; storeMessageKeys(LOCAL_HIDDEN_MESSAGE_STORAGE_KEY, conversation.id, items, userId); const messagesByConversation = readLocalMessages(userId); messagesByConversation[conversation.id] = applyLocalMessageState( conversation.id, messagesByConversation[conversation.id] || [], userId ); writeLocalMessages(messagesByConversation, userId); } markMessagesRecalled(conversation, messages) { const items = Array.isArray(messages) ? messages : [messages]; if (!conversation?.id || !items.length) { return; } const userId = this.currentUser?.userId || ''; storeMessageKeys(LOCAL_RECALLED_MESSAGE_STORAGE_KEY, conversation.id, items, userId); const messagesByConversation = readLocalMessages(userId); messagesByConversation[conversation.id] = applyLocalMessageState( conversation.id, messagesByConversation[conversation.id] || [], userId ); writeLocalMessages(messagesByConversation, userId); } async recallMessage(conversation, message) { const messageUid = String(message?.messageUid || message?.id || '').trim(); if (!/^\d+$/.test(messageUid)) { throw new Error('缺少可撤回的消息ID'); } const response = await axios.post(appConfig.messageActionApi, { action: 'recall', userId: this.currentUser?.userId || message.from || '', messageUid }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '撤回失败'); } this.markMessagesRecalled(conversation, [message]); return response.data.result; } async editMessage(conversation, message, text) { const messageUid = String(message?.messageUid || message?.id || '').trim(); if (!/^\d+$/.test(messageUid)) { throw new Error('缺少可编辑的消息ID'); } const nextText = String(text || '').trim(); if (!nextText) { throw new Error('编辑内容不能为空'); } const response = await axios.post(appConfig.messageActionApi, { action: 'edit', userId: this.currentUser?.userId || message.from || '', messageUid, text: nextText }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '编辑失败'); } const result = response.data?.result?.message || {}; const editedMessage = { ...message, text: result.text || nextText, status: 'delivered', deliveryStatus: 'delivered', editedAt: Number(result.editedAt || Date.now()), activityTimestamp: Number(result.editedAt || Date.now()) }; saveLocalMessage(editedMessage, this.currentUser?.userId || ''); this.emit('messageStatusChanged', editedMessage); return editedMessage; } async getMessageDetail(conversation, message) { const messageUid = String(message?.messageUid || message?.id || '').trim(); if (!messageUid) { throw new Error('缺少消息ID'); } const response = await axios.post(appConfig.messageDetailApi, { userId: this.currentUser?.userId || '', target: conversation?.target || conversation?.raw?.target || conversation?.id?.replace(/^single-/, '') || '', conversationType: conversation?.type || conversation?.raw?.type || 'single', messageUid }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '消息详情加载失败'); } return response.data?.result?.detail || null; } async sendTextMessage(conversation, text, initialMessage = null) { const senderRole = conversation?.role || conversation?.raw?.role || ''; const message = initialMessage || { id: randomId(), conversationId: conversation.id, from: this.currentUser?.userId || 'me', senderRole, direction: 'out', type: 'text', text, status: 'sending', deliveryStatus: 'sending', timestamp: Date.now() }; saveLocalMessage(message, this.currentUser?.userId || ''); if (!initialMessage) { this.emit('messageSent', message); } try { const response = await axios.post(appConfig.sendMessageApi, { sender: this.currentUser?.userId || '', target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''), conversationType: conversation.type || conversation.raw?.type || 0, text }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '发送失败'); } const sentMessage = { ...message, status: 'sent', deliveryStatus: 'sent', messageUid: response.data?.result?.messageUid ? String(response.data.result.messageUid) : '', senderRole: response.data?.result?.senderRole || message.senderRole || '', serverTimestamp: Number(response.data?.result?.timestamp || 0) || null, timestamp: message.timestamp, displayTimestamp: message.displayTimestamp || message.timestamp }; saveLocalMessage(sentMessage, this.currentUser?.userId || ''); this.emit('messageStatusChanged', sentMessage); return sentMessage; } catch (error) { const failedMessage = { ...message, status: 'failed', deliveryStatus: '', error: error.response?.data?.message || error.message || '发送失败' }; saveLocalMessage(failedMessage, this.currentUser?.userId || ''); this.emit('messageStatusChanged', failedMessage); return failedMessage; } } async sendAttachmentMessage(conversation, { type, file, text = '' }, initialMessage = null) { const senderRole = conversation?.role || conversation?.raw?.role || ''; const message = initialMessage || { id: randomId(), conversationId: conversation.id, from: this.currentUser?.userId || 'me', senderRole, direction: 'out', type, text, attachment: { type, name: file?.name || (type === 'image' ? '图片' : '文件'), mime: file?.type || '', size: file?.size || 0, url: type === 'image' && file ? URL.createObjectURL(file) : '' }, status: 'sending', deliveryStatus: 'sending', timestamp: Date.now() }; saveLocalMessage(message, this.currentUser?.userId || ''); if (!initialMessage) { this.emit('messageSent', message); } try { const formData = new FormData(); formData.append('sender', this.currentUser?.userId || ''); formData.append('target', conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, '')); formData.append('conversationType', conversation.type || conversation.raw?.type || 0); formData.append('messageType', type); formData.append('text', text || ''); formData.append('attachment', file); const response = await axios.post(appConfig.sendMessageApi, formData, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '发送失败'); } const sentMessage = { ...message, status: 'sent', deliveryStatus: 'sent', messageUid: response.data?.result?.messageUid ? String(response.data.result.messageUid) : '', senderRole: response.data?.result?.senderRole || message.senderRole || '', attachment: response.data?.result?.attachment || message.attachment, serverTimestamp: Number(response.data?.result?.timestamp || 0) || null, timestamp: message.timestamp, displayTimestamp: message.displayTimestamp || message.timestamp }; saveLocalMessage(sentMessage, this.currentUser?.userId || ''); this.emit('messageStatusChanged', sentMessage); return sentMessage; } catch (error) { const failedMessage = { ...message, status: 'failed', deliveryStatus: '', error: error.response?.data?.message || error.message || '发送失败' }; saveLocalMessage(failedMessage, this.currentUser?.userId || ''); this.emit('messageStatusChanged', failedMessage); return failedMessage; } } getContactList() { return []; } async getServerContactList(userId) { if (!userId) { return []; } const response = await axios.post(appConfig.contactsApi, { userId }, { withCredentials: false, headers: this.getAppAuthHeaders(appConfig.contactsApi) }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '联系人加载失败'); } return (response.data?.result?.contacts || []).map(item => { return this.mapContact(item.uid, { alias: item.alias }, { uid: item.uid, displayName: item.displayName, name: item.name, mobile: item.mobile, portrait: item.portrait, address: item.address, friendAlias: item.alias }); }).sort((a, b) => a.title.localeCompare(b.title, 'zh-CN')); } async addContact(userId, keyword, reason = '') { const response = await axios.post(appConfig.addContactApi, { userId, keyword, reason }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '添加联系人失败'); } const item = response.data?.result?.contact; if (!item?.uid) { return null; } return this.mapContact(item.uid, { alias: item.alias }, { uid: item.uid, displayName: item.displayName, name: item.name, mobile: item.mobile, portrait: item.portrait, address: item.address, friendAlias: item.alias }); } normalizeConversation(conversation) { const title = getTitle(conversation?.title, conversation?.target || ''); return { ...conversation, id: conversation?.id || `${conversation?.type === 'single' ? 'single' : 'group'}-${conversation?.target}`, target: conversation?.target || conversation?.raw?.target || '', title, subtitle: conversation?.subtitle || '', unread: Number(conversation?.unread || 0), portrait: conversation?.portrait || '', avatarText: conversation?.avatarText || title.slice(0, 1).toUpperCase(), updatedAt: Number(conversation?.updatedAt || Date.now()), raw: conversation?.raw || { type: conversation?.type === 'single' ? 'single' : 'group', target: conversation?.target || '', line: 0 } }; } async createGroup(userId, { name, memberIds = [], announcement = '', welcomeText = '', channel = false }) { const response = await axios.post(appConfig.conversationActionsApi, { action: channel ? 'create_channel' : 'create_group', userId, name, memberIds, announcement, welcomeText }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || (channel ? '创建公告群失败' : '创建群组失败')); } return this.normalizeConversation(response.data?.result?.conversation || {}); } async joinGroup(userId, groupKey) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'join_group', userId, groupKey }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '加入群组失败'); } return this.normalizeConversation(response.data?.result?.conversation || {}); } async broadcastMessage(userId, targetIds, text) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'broadcast', userId, targetIds, text }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '群发失败'); } return response.data?.result || {}; } async getGroupDetail(userId, groupId) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'group_detail', userId, groupId }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '群资料加载失败'); } return response.data?.result?.group || null; } async getGroupMembers(userId, groupId) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'group_members', userId, groupId }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '群员加载失败'); } return response.data?.result?.members || []; } async updateGroupAlias(userId, groupId, alias) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'update_group_alias', userId, groupId, alias }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '本群昵称保存失败'); } return response.data?.result?.alias || ''; } async leaveGroup(userId, groupId) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'leave_group', userId, groupId }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '退出群组失败'); } return response.data?.result || {}; } async dismissGroup(userId, groupId) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'dismiss_group', userId, groupId }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '解散群组失败'); } return response.data?.result || {}; } async transferGroup(userId, groupId, targetUserId) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'transfer_group', userId, groupId, targetUserId }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '转让群组失败'); } return response.data?.result || {}; } async setGroupAdmins(userId, groupId, adminIds) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'set_group_admins', userId, groupId, adminIds }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '管理员设置失败'); } return response.data?.result || {}; } async updateGroupProfile(userId, groupId, { name, announcement, welcomeText }) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'update_group_profile', userId, groupId, name, announcement, welcomeText }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '群资料保存失败'); } return response.data?.result || {}; } async kickGroupMembers(userId, groupId, targetIds) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'kick_group_members', userId, groupId, targetIds }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '踢人失败'); } return response.data?.result || {}; } async updateGroupMuteSetting(userId, groupId, setting) { const response = await axios.post(appConfig.conversationActionsApi, { action: 'update_mute_setting', userId, groupId, ...setting }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '禁言设置保存失败'); } return response.data?.result?.muteSetting || null; } async updateContactAlias(userId, targetId, alias) { const response = await axios.post(appConfig.updateAliasApi, { userId, targetId, alias }, { withCredentials: false, headers: this.getAppAuthHeaders(appConfig.updateAliasApi) }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '备注名修改失败'); } return response.data?.result?.contact?.alias || ''; } async getProfile(userId) { const response = await axios.post(appConfig.profileApi, { action: 'get', userId }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '用户资料加载失败'); } return response.data?.result?.profile || null; } async updateProfile(userId, { displayName, portraitFile }) { const formData = new FormData(); formData.append('action', 'update'); formData.append('userId', userId); formData.append('displayName', displayName); if (portraitFile) { formData.append('portraitFile', portraitFile); } const response = await axios.post(appConfig.profileApi, formData, { withCredentials: false, headers: this.getAppAuthHeaders(appConfig.appServer) }); if (response.data?.code !== 0) { throw new Error(response.data?.message || '用户资料保存失败'); } return response.data?.result?.profile || null; } async changePassword({ oldPassword, newPassword }) { const response = await axios.post(appConfig.changePasswordApi, { oldPassword, newPassword }, { withCredentials: false, headers: this.getAppAuthHeaders(appConfig.changePasswordApi) }); if (response.data?.code !== 0) { throw new Error(response.data?.message || response.data?.msg || '密码修改失败'); } return true; } async resetPasswordWithCode({ mobile, resetCode, newPassword }) { const response = await axios.post(appConfig.resetPasswordApi, { mobile, resetCode, newPassword }, { withCredentials: false }); if (response.data?.code !== 0) { throw new Error(response.data?.message || response.data?.msg || '密码设置失败'); } return true; } refreshContactList() { const contacts = this.getContactList(); this.emit('contactsChanged', contacts); return contacts; } createSingleConversation(userId, userInfo = null) { const title = getTitle(userInfo?.title || userInfo?.displayName || userInfo?.name, userId); return { id: `single-${userId}`, type: 'single', target: userId, title, subtitle: '', unread: 0, portrait: userInfo?.portrait || '', avatarText: title.slice(0, 1).toUpperCase(), updatedAt: Date.now(), raw: { type: 'single', target: userId, line: 0 } }; } mapContact(userId, friend, userInfo) { const title = getTitle(userInfo?.friendAlias || friend?.alias || userInfo?.displayName || userInfo?.name, userId); return { id: userId, uid: userId, title, subtitle: userInfo?.mobile || userInfo?.name || userId, portrait: userInfo?.portrait || '', address: userInfo?.address || '', avatarText: title.slice(0, 1).toUpperCase(), alias: friend?.alias || userInfo?.friendAlias || '', rawFriend: friend, rawUser: userInfo }; } on(eventName, handler) { if (!this.listeners.has(eventName)) { this.listeners.set(eventName, new Set()); } this.listeners.get(eventName).add(handler); return () => this.off(eventName, handler); } off(eventName, handler) { this.listeners.get(eventName)?.delete(handler); } emit(eventName, payload) { this.listeners.get(eventName)?.forEach(handler => handler(payload)); } persistAppToken(url, response) { const appToken = response.headers?.authtoken || response.headers?.authToken; if (appToken) { localStorage.setItem(appConfig.authTokenStoragePrefix + this.getUrlHost(url), appToken); } } getAppAuthHeaders(url) { const host = this.getUrlHost(url); if (!host) { return {}; } const token = localStorage.getItem(appConfig.authTokenStoragePrefix + host); return token ? { authToken: token } : {}; } getUrlHost(url) { try { return new URL(url, window.location.origin).host; } catch (error) { return ''; } } } export const imClient = new HttpImClient();