Files
IM-web-new/legacy/imClient.legacy.js
T
2026-10-09 22:49:53 +07:00

652 lines
21 KiB
JavaScript

import axios from 'axios';
import { appConfig } from '@/config';
function randomId() {
if (crypto && crypto.randomUUID) {
return crypto.randomUUID().replaceAll('-', '');
}
return `${Date.now()}${Math.random().toString(16).slice(2)}`;
}
function getAllowedDevice(mobile) {
const allowedDevices = {
'18301902119': '67072d37062c4f31b1165073552933ba',
'18301902118': 'de89fb2f2a61480a8c381f6c16e28bd8',
'18301902117': 'd117f480faa64145b6162107b1b49617'
};
return allowedDevices[mobile] || '';
}
function getUniqueEquipment(mobile, fallback) {
return getAllowedDevice(mobile) || localStorage.getItem('uniqueEquipment') || fallback;
}
function normalizeStatus(message) {
if (message.direction !== 0) {
return 'sent';
}
if (message.status === 0 || message.status === 'sending') {
return 'sending';
}
if (message.status === 2 || message.status === 'failed' || message.status === -1) {
return 'failed';
}
return 'sent';
}
function sendErrorText(errorCode) {
const messages = {
'-1': '连接不可用或发送超时',
1: '密钥不匹配',
2: '无效数据',
4: '服务端错误',
6: 'token错误',
7: '账号被踢下线',
8: '用户被禁用',
9: '不在群组中',
10: '无效消息',
22: '服务未授权',
28: 'WebSocket未配置',
30: '服务器时间不同步',
245: '用户被拉黑',
246: '在黑名单中',
247: '禁止发送消息',
248: '无权限',
249: '服务端超时',
250: '发送过于频繁',
251: '参数无效',
253: '目标不存在',
254: '服务未实现'
};
return messages[errorCode] || `错误码 ${errorCode}`;
}
function resolveConversationTarget(conversation) {
if (!conversation) {
return '';
}
return conversation.target
|| conversation.raw?.target
|| String(conversation.id || '').replace(/^single-/, '').replace(/^0-/, '').replace(/-0$/, '');
}
export class ImClient {
constructor() {
this.listeners = new Map();
this.connected = false;
this.currentUser = null;
this.sdkReady = false;
this.sdk = null;
this.sdkModules = null;
this.sdkEventsBound = false;
this.pendingOutgoingMessages = new Map();
this.loginClientId = '';
}
getClientId() {
if (this.sdkReady && this.sdk) {
return this.sdk.getClientId();
}
let clientId = localStorage.getItem(appConfig.clientIdStorageKey);
if (!clientId) {
clientId = randomId();
localStorage.setItem(appConfig.clientIdStorageKey, clientId);
}
return clientId;
}
getSdkVersion() {
if (!this.sdkReady || !this.sdk || typeof this.sdk.getVersion !== 'function') {
return '';
}
return this.sdk.getVersion() || '';
}
prepareClientIdForLogin(mobile) {
const pinnedClientId = getAllowedDevice(mobile) || localStorage.getItem('uniqueEquipment') || '';
if (pinnedClientId) {
sessionStorage.setItem('clientId', pinnedClientId);
localStorage.setItem('clientId', pinnedClientId);
localStorage.setItem(appConfig.clientIdStorageKey, pinnedClientId);
this.loginClientId = pinnedClientId;
return pinnedClientId;
}
this.loginClientId = this.getClientId();
return this.loginClientId;
}
restoreClientId(clientId) {
if (!clientId) {
return;
}
sessionStorage.setItem('clientId', clientId);
localStorage.setItem('clientId', clientId);
localStorage.setItem(appConfig.clientIdStorageKey, clientId);
this.loginClientId = clientId;
}
async loginWithPassword(mobile, password) {
const deviceId = this.prepareClientIdForLogin(mobile);
await this.ensureSdk();
const url = `${appConfig.appServer}/login_pwd`;
const sdkClientId = this.getClientId();
if (sdkClientId !== deviceId) {
console.warn('[im] clientId mismatch before login', { deviceId, sdkClientId });
}
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) {
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 deviceId = this.prepareClientIdForLogin(mobile);
await this.ensureSdk();
const url = `${appConfig.appServer}/login`;
const sdkClientId = this.getClientId();
if (sdkClientId !== deviceId) {
console.warn('[im] clientId mismatch before login', { deviceId, sdkClientId });
}
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 connect(userId, token) {
this.currentUser = { userId, token };
await this.ensureSdk();
console.info('[im] connect', {
userId,
clientId: this.getClientId(),
configuredClientId: this.loginClientId || localStorage.getItem(appConfig.clientIdStorageKey) || ''
});
this.sdk.connect(userId, token);
this.emit('connectionStatusChanged', { connected: false, statusText: '连接中' });
}
disconnect() {
if (this.sdkReady && this.sdk) {
this.sdk.disconnect();
}
this.connected = false;
this.currentUser = null;
this.emit('connectionStatusChanged', { connected: false });
}
sendTextMessage(conversation, text, initialMessage = null) {
const target = resolveConversationTarget(conversation);
const rawConversation = conversation.raw || this.createRawSingleConversation(target);
if (this.sdkReady && this.sdk && rawConversation) {
const { TextMessageContent } = this.sdkModules;
const content = new TextMessageContent(text);
console.info('[im] send text', {
conversationId: conversation.id,
target: rawConversation.target,
type: rawConversation.type,
line: rawConversation.line,
connected: this.connected
});
this.sdk.sendConversationMessage(
rawConversation,
content,
[],
(messageId, timestamp) => {
if (initialMessage) {
this.pendingOutgoingMessages.set(String(messageId), {
...initialMessage,
sdkMessageId: String(messageId),
sdkTimestamp: Number(timestamp || Date.now())
});
}
console.info('[im] send prepared', {
conversationId: conversation.id,
target: rawConversation.target,
messageId,
timestamp
});
if (!initialMessage) {
this.emit('messageSent', this.mapOutgoingMessage(conversation.id, messageId, timestamp, text, 'sending'));
}
},
null,
(messageUid, timestamp) => {
console.info('[im] send success', {
conversationId: conversation.id,
target: rawConversation.target,
messageUid,
timestamp
});
this.emit('messageStatusChanged', {
...(initialMessage || {}),
conversationId: conversation.id,
id: initialMessage?.id || String(messageUid || randomId()),
messageUid,
timestamp: initialMessage?.timestamp || Number(timestamp || Date.now()),
displayTimestamp: initialMessage?.displayTimestamp || initialMessage?.timestamp || Number(timestamp || Date.now()),
status: 'sent'
});
},
errorCode => {
const failedMessage = {
...(initialMessage || {}),
conversationId: conversation.id,
id: initialMessage?.id || randomId(),
text,
direction: 'out',
type: 'text',
status: 'failed',
errorCode,
error: sendErrorText(errorCode)
};
this.updatePendingOutgoingMessage(failedMessage);
console.error('[im] send failed', {
conversationId: conversation.id,
target: rawConversation.target,
connected: this.connected,
errorCode,
error: sendErrorText(errorCode)
});
this.emit('messageStatusChanged', failedMessage);
}
);
return null;
}
const message = {
id: randomId(),
conversationId: conversation.id,
from: this.currentUser?.userId || 'me',
direction: 'out',
type: 'text',
text,
status: 'failed',
timestamp: Date.now(),
error: rawConversation ? 'SDK未就绪' : `会话目标无效:${target || '空'}`
};
this.emit('messageSent', message);
return message;
}
getConversationList() {
if (!this.sdkReady || !this.sdk) {
return [];
}
const { ConversationType } = this.sdkModules;
return this.sdk.getConversationList(
[ConversationType.Single, ConversationType.Group, ConversationType.Channel],
[0]
).map(item => this.mapConversationInfo(item));
}
refreshConversationList() {
const conversations = this.getConversationList();
this.emit('conversationsChanged', conversations);
return conversations;
}
getMessages(conversation, count = 30) {
const rawConversation = conversation.raw || this.createRawSingleConversation(resolveConversationTarget(conversation));
if (!this.sdkReady || !this.sdk || !rawConversation) {
return [];
}
return this.sdk.getMessages(rawConversation, 0, true, count).map(message => this.mapMessage(message));
}
async fetchRemoteMessages(conversation, limit = 50) {
if (!conversation?.id || !this.currentUser?.userId) {
return this.getMessages(conversation, limit);
}
const response = await axios.post(appConfig.messagesApi, {
userId: this.currentUser.userId,
target: resolveConversationTarget(conversation),
limit
}, { withCredentials: false });
if (response.data?.code !== 0) {
throw new Error(response.data?.message || '消息同步失败');
}
return (response.data?.result?.messages || []).map(message => ({
...message,
displayTimestamp: message.displayTimestamp || message.timestamp,
conversationId: conversation.id
}));
}
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));
}
updatePendingOutgoingMessage(message) {
if (!message?.id) {
return;
}
this.pendingOutgoingMessages.forEach((pendingMessage, sdkMessageId) => {
if (pendingMessage.id === message.id) {
this.pendingOutgoingMessages.set(sdkMessageId, {
...pendingMessage,
...message,
sdkMessageId
});
}
});
}
mapSdkStatusMessage(message) {
const mappedMessage = this.mapMessage(message);
if (message.direction !== 0 || !message.messageId) {
return mappedMessage;
}
const sdkMessageId = String(message.messageId);
const pendingMessage = this.pendingOutgoingMessages.get(sdkMessageId);
if (!pendingMessage) {
return mappedMessage;
}
const nextMessage = {
...pendingMessage,
...mappedMessage,
id: pendingMessage.id,
conversationId: pendingMessage.conversationId || mappedMessage.conversationId,
text: pendingMessage.text || mappedMessage.text,
timestamp: pendingMessage.timestamp || mappedMessage.timestamp,
displayTimestamp: pendingMessage.displayTimestamp || mappedMessage.displayTimestamp,
sdkMessageId,
errorCode: mappedMessage.errorCode || pendingMessage.errorCode,
error: mappedMessage.error || pendingMessage.error
};
this.pendingOutgoingMessages.set(sdkMessageId, nextMessage);
if (nextMessage.status === 'sent') {
window.setTimeout(() => this.pendingOutgoingMessages.delete(sdkMessageId), 60000);
}
return nextMessage;
}
async ensureSdk() {
if (this.sdkReady || appConfig.sdkMode !== 'wildfire') {
return;
}
const wfcModule = await import('legacy-wfc/client/wfc');
const configModule = await import('legacy-config');
const eventModule = await import('legacy-wfc/client/wfcEvent');
const connectionStatusModule = await import('legacy-wfc/client/connectionStatus');
const conversationTypeModule = await import('legacy-wfc/model/conversationType');
const conversationModule = await import('legacy-wfc/model/conversation');
const textMessageModule = await import('legacy-wfc/messages/textMessageContent');
const Config = configModule.default;
Config.APP_SERVER = appConfig.appServer;
Config.USE_WSS = appConfig.useWss;
Config.ROUTE_PORT = appConfig.imRoutePort;
Config.WS_PATH = appConfig.wsPath;
Config.HTTP_IM_PREFIX_PATH = '';
Config.HTTP_UPLOAD_PREFIX_PATH = '';
Config.CLIENT_ID_STRATEGY = 2;
this.sdk = wfcModule.default;
this.sdkModules = {
EventType: eventModule.default,
ConnectionStatus: connectionStatusModule.default,
ConversationType: conversationTypeModule.default,
Conversation: conversationModule.default,
TextMessageContent: textMessageModule.default
};
this.bindSdkEvents();
this.sdk.init();
this.sdkReady = true;
}
bindSdkEvents() {
if (this.sdkEventsBound) {
return;
}
this.sdkEventsBound = true;
const { EventType, ConnectionStatus } = this.sdkModules;
this.sdk.eventEmitter.on(EventType.ConnectionStatusChanged, status => {
const connected = status === ConnectionStatus.ConnectionStatusConnected;
this.connected = connected;
this.emit('connectionStatusChanged', {
connected,
status,
statusText: ConnectionStatus.desc(status) || '连接状态变化'
});
if (connected || status === ConnectionStatus.ConnectionStatusReceiveing) {
window.setTimeout(() => this.refreshConversationList(), 0);
}
});
this.sdk.eventEmitter.on(EventType.ConversationInfoUpdate, () => {
this.refreshConversationList();
});
this.sdk.eventEmitter.on(EventType.FriendListUpdate, () => {
this.emit('contactsChanged', this.getContactList(true));
});
this.sdk.eventEmitter.on(EventType.UserInfosUpdate, () => {
this.emit('contactsChanged', this.getContactList(false));
});
this.sdk.eventEmitter.on(EventType.MessageStatusUpdate, message => {
this.emit('messageStatusChanged', this.mapSdkStatusMessage(message));
});
this.sdk.eventEmitter.on(EventType.ReceiveMessage, message => {
this.emit('messageReceived', this.mapMessage(message));
this.refreshConversationList();
});
}
mapConversationInfo(info) {
const conversation = info.conversation;
const id = `${conversation.type}-${conversation.target}-${conversation.line}`;
const title = this.getConversationTitle(info);
return {
id,
type: conversation.type,
title,
subtitle: info.lastMessage ? this.messageDigest(info.lastMessage) : '',
unread: info.unreadCount?.unread || info.unreadCount || 0,
avatarText: title.slice(0, 1).toUpperCase(),
updatedAt: Number(info.timestamp || info.lastMessage?.timestamp || Date.now()),
raw: conversation,
rawInfo: info
};
}
getContactList(refresh = false) {
if (!this.sdkReady || !this.sdk) {
return [];
}
const friendIds = this.sdk.getMyFriendList(refresh) || [];
if (!friendIds.length) {
return [];
}
const userInfos = this.sdk.getUserInfos(friendIds, '') || [];
return userInfos.map(userInfo => {
return this.mapContact(userInfo.uid, null, userInfo);
}).sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'));
}
async getServerContactList(userId) {
if (!userId) {
return [];
}
const response = await axios.post(appConfig.contactsApi, { userId }, { withCredentials: false });
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,
friendAlias: item.alias
});
}).sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'));
}
refreshContactList() {
const contacts = this.getContactList(true);
this.emit('contactsChanged', contacts);
return contacts;
}
createRawSingleConversation(userId) {
if (!userId || !this.sdkModules?.Conversation || !this.sdkModules?.ConversationType) {
return null;
}
const { Conversation, ConversationType } = this.sdkModules;
return new Conversation(ConversationType.Single, userId, 0);
}
createSingleConversation(userId) {
if (!this.sdkReady || !this.sdk) {
const title = userId;
return {
id: `single-${userId}`,
type: 'single',
target: userId,
title,
subtitle: '',
unread: 0,
avatarText: title.slice(0, 1).toUpperCase(),
updatedAt: Date.now(),
raw: null
};
}
const { ConversationType } = this.sdkModules;
const raw = this.createRawSingleConversation(userId);
const info = this.sdk.getConversationInfo(raw);
return info ? this.mapConversationInfo(info) : {
id: `${ConversationType.Single}-${userId}-0`,
type: ConversationType.Single,
target: userId,
title: this.sdk.getUserDisplayName(userId),
subtitle: '',
unread: 0,
avatarText: this.sdk.getUserDisplayName(userId).slice(0, 1).toUpperCase(),
updatedAt: Date.now(),
raw
};
}
mapContact(userId, friend, userInfo) {
const title = userInfo?.friendAlias || friend?.alias || userInfo?.displayName || userInfo?.name || userId;
return {
id: userId,
uid: userId,
title,
subtitle: userInfo?.mobile || userInfo?.name || userId,
portrait: userInfo?.portrait || '',
avatarText: title.slice(0, 1).toUpperCase(),
alias: friend?.alias || userInfo?.friendAlias || '',
rawFriend: friend,
rawUser: userInfo
};
}
getConversationTitle(info) {
const conversation = info.conversation;
if (conversation._target?._displayName) {
return conversation._target._displayName;
}
if (conversation.type === this.sdkModules.ConversationType.Single) {
return this.sdk.getUserDisplayName(conversation.target);
}
if (conversation.type === this.sdkModules.ConversationType.Group) {
return this.sdk.getGroupInfo(conversation.target)?.name || conversation.target;
}
return conversation.target;
}
mapMessage(message) {
return {
id: String(message.messageId || message.messageUid || randomId()),
messageUid: message.messageUid,
conversationId: message.conversation ? `${message.conversation.type}-${message.conversation.target}-${message.conversation.line}` : '',
from: message.from,
direction: message.direction === 0 ? 'out' : 'in',
type: 'text',
text: this.messageDigest(message),
status: normalizeStatus(message),
timestamp: Number(message.timestamp || Date.now()),
displayTimestamp: Number(message.timestamp || Date.now()),
raw: message
};
}
mapOutgoingMessage(conversationId, messageId, timestamp, text, status) {
return {
id: String(messageId || randomId()),
conversationId,
from: this.currentUser?.userId || 'me',
direction: 'out',
type: 'text',
text,
status,
timestamp: Number(timestamp || Date.now())
};
}
messageDigest(message) {
if (!message?.messageContent) {
return '';
}
if (message.messageContent.content) {
return message.messageContent.content;
}
if (typeof message.messageContent.digest === 'function') {
return message.messageContent.digest(message);
}
return '[暂不支持的消息]';
}
persistAppToken(url, response) {
const appToken = response.headers?.authtoken || response.headers?.authToken;
if (appToken) {
localStorage.setItem(appConfig.authTokenStoragePrefix + new URL(url).host, appToken);
}
}
}
export const imClient = new ImClient();