Files
IM-web-new/src/services/httpImClient.js
T

1037 lines
32 KiB
JavaScript
Raw Normal View History

2026-06-30 10:13:48 +07:00
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();