import type { ConnectConfig, ConnectConfigSettingParams } from './interface/manager'; import Socket from './socket'; import Session from './session'; import { LogType } from './interface/common'; import type { MIND_MODE, SessionConfig, SessionDataInfo, SessionInterface, SessionLifeCycleModel, SessionMessageModel, SessionMindInfo, SessionSendModel, WS_MIND_TYPE } from './interface/session'; import { ChatSessionSubCodeAction, FRONTEND_ERROR_CODE, isSessionMessage } from './interface/session'; import { SocketChannelType, SocketError, WS_MSG_TYPE } from './interface/socket'; import type { CreateSessionProps, CreateTaskConfig, SessionModelItem } from './interface/socket-session-manager'; import { isTaskMessage, type TaskConfig, type TaskInterface, type TaskMessageModel } from './interface/task'; import Task from './tasks'; export interface CreateSessionResult { session: SessionInterface; retryCount?: number; // 如果为空,说明是老的 session openSessionTime?: number; // 如果为空,说明是老的 session confirmSessionTime?: number; // 如果为空,说明是老的 session } /** * 用于管理 Socket 和 Session 的关系 * 两者的关系应该是多对多的关系,一个 Socket 可以有多个 Session * 多个 Socket 也可以有同一个 Session。(因为会存在 Socket 断链,需要重新链接的情况 */ export default class SocketSessionManager { /** * 管理 socket 的实例,在一个 web 当中应该仅维护一个。 * * @private * @type {Socket} * @memberof SocketSessionManager */ private _socket: Socket; /** * 管理 session 的实例,key 为 session id * * @private * @type {Record} * @memberof SocketSessionManager */ private _sessionMap: Record = {}; /** * 用于所有的参数配置 * * @private * @type {ConnectConfigSettingParams} * @memberof SocketSessionManager */ private _config?: ConnectConfigSettingParams; /** * 是否展示 debug 信息 * * @private * @type {boolean} * @memberof SocketSessionManager */ private _showDebug = false; private _unSavedCallback: CreateTaskConfig[] = []; /** * 正在创建 session */ private creatingSessionList: SessionMindInfo[] = []; /** * config 配置信息,这里使用 getter 是因为只读,防止被外部修改 * * @readonly * @type {ConnectConfig} * @memberof SocketSessionManager */ get config(): ConnectConfig { return { baseURL: this._config?.baseURL ?? 'https://mindos-devusk8s.mindverse.ai/gate', requestHeader: { platform: '', appId: '', bizType: '', merchantId: '', 'M-AuthType': '', ...this._config?.requestHeader }, refUserId: this._config?.refUserId ?? '', APIVersion: '1.3.0', socketURLConfig: { socketPath: '/chat/rest/general/ws/create', socketCheckPath: '/chat/rest/general/ws/get', channel: [SocketChannelType.all], ...this._config?.socketURLConfig }, sessionURLConfig: { openPath: '/chat/rest/general/session/create', closePath: '/chat/rest/general/session/close', sendMsgPath: '/chat/rest/general/message/send', fetchMsgPath: '/chat/rest/general/message/query', fetchLastMsgPath: '/chat/rest/general/message/latest', ...this._config?.sessionURLConfig }, sessionConnectConfig: { deviceId: 'deviceId', ...this._config?.sessionConnectConfig }, noticeConfig: this._config?.noticeConfig || {} }; } /** * 还存在的 session 信息 * * @readonly * @type {SessionDataInfo[]} * @memberof SocketSessionManager */ get sessionDetailList(): Record { return Object.entries(this._sessionMap).reduce( (prev, [key, value]) => ({ ...prev, [key]: value.session.sessionData }), {} ); } // 构造函数中在创建SSM实例的时候就全局地创建socket实例 constructor(data: { config?: ConnectConfigSettingParams; showDebug: boolean; }) { this._config = data.config; this._showDebug = data.showDebug; this._socket = new Socket({ baseUrl: this.config.baseURL, header: this.config.requestHeader, urlConfig: this.config.socketURLConfig, lifeCycle: { onMessage: (msg) => { if (msg.type === WS_MSG_TYPE.notice) { // 直接透传,类型啥的定在外部 this.config.noticeConfig.onNoticeMessage?.(msg); return; } const newMsg = msg as SessionMessageModel; const existSession = this.hasSessionId(newMsg.sessionId); if (!existSession) { // session 不存在的情况,外部露出,用户侧不体现 this._log(LogType.error, '[manager] session is not exist', msg); return; } if (msg.seqId) { this._socket.sendAck(msg.seqId); } // 将 socket 级别消息转换成 session 级别的消息,后续可以做校验逻辑 if (isTaskMessage(msg)) { const task = this.getTaskWithSessionId(newMsg.sessionId); task?.appendTask(msg as TaskMessageModel); } else if (isSessionMessage(msg)) { const session = this.getSessionWithSessionId(newMsg.sessionId); session?.receiveMsgFromSocket(newMsg); } }, onOpen: this._socketOpenCallback, onClose: this._socketCloseCallback, onError: this._socketErrorCallback }, APIVersion: this.config.APIVersion, refUserId: this.config.refUserId, log: this._log }); this._socket.build().catch((error) => { this._log(LogType.error, '[manager] socket build error', error); }); } /** * 对外暴露,用于重置SSM实例的时候清除管理的socket和session * @returns */ destructor(): void { this._socket.close(); Object.keys(this._sessionMap).forEach((id) => { this.closeSession(id); }); } /** * 对外暴露,根据当前session信息查询历史消息 * @returns createSession() 返回的 session 基本信息 */ fetchHistoryMessage(data: CreateSessionProps): Promise { const session = this._createPrivateSession(data); return session.fetchHistoryMessage(data.originSessionId); } /** * 对外暴露,用于创建 Session * @returns createSession() 返回的 session 基本信息 */ createSession(data: CreateSessionProps): Promise { // build 里面完成 socket 检测,重连逻辑,绑定这里关心完成之后的联动逻辑。 const { mindId, mindType, mode, originSessionId } = data; // socket 和 session 并行,互不影响 this._socket.build().catch((error) => { this._log(LogType.error, '[manager] socket build error', error); }); const existSession = Object.values(this._sessionMap).find((item) => { const sessionData = item.session.sessionData; return ( sessionData.mindId === mindId && sessionData.mindType === mindType && sessionData.mode === mode ); })?.session; // 如果已经存在且不是要续接老session,则直接复用,不需要重新创建 if (existSession && !originSessionId) { existSession.waitingForStart(); existSession.callback = data.callback; // 已经存在的时候,先加载 history 然后在返回 return Promise.resolve({ ...existSession.sessionData }); } const isCreatingSession = this.creatingSessionList.find( (item) => item.mindId === mindId && item.mindType === mindType && item.mode === mode ); if (isCreatingSession) { return Promise.reject(new Error('session is Creating')); } // 创建前先 cache 正在创建的 session this.creatingSessionList.push({ mindId: mindId, mindType: mindType, mode: mode }); return this._createSession(data).then( ({ session, openSessionTime, retryCount, confirmSessionTime }) => { const sessionId = session.sessionData.sessionId; const oldTask = this.getTaskWithSessionId(sessionId); const findTaskItem = this._unSavedCallback.find((item) => { return ( item.mindInfo.mindId === mindId && item.mindInfo.mindType === mindType && item.mindInfo.mode === mode ); }); let newSessionModel: SessionModelItem = { session, task: oldTask }; if (findTaskItem) { const { callback, mindInfo } = findTaskItem; const newTask = new Task({ callback, mindInfo, sessionId, urlConfig: { baseUrl: this.config.baseURL, refUserId: this.config.refUserId, header: this.config.requestHeader } }); newTask.startReceiveMsg(); newSessionModel = { session, task: newTask }; this._unSavedCallback = this._unSavedCallback.filter((item) => { return !( item.mindInfo.mindId === mindId && item.mindInfo.mindType === mindType && item.mindInfo.mode === mode ); }); } // 移除正在创建的 session 信息 this.creatingSessionList = this.creatingSessionList.filter( (item) => !( item.mindId === mindId && item.mindType === mindType && item.mode === mode ) ); this._sessionMap[sessionId] = newSessionModel; return { ...session.sessionData, openSessionTime, retryCount, confirmSessionTime }; } ); } createTask(config: Omit, 'urlConfig'>): void { const { mindInfo, callback } = config; const existSessionModel = this.getSessionModelWithMindInfo(mindInfo); if (existSessionModel?.task) { existSessionModel.task.callback = callback; return; } if (existSessionModel) { // 开了 session 没有开 task const { session } = existSessionModel; const task = new Task({ callback, mindInfo, sessionId: session.sessionData.sessionId, urlConfig: { baseUrl: this.config.baseURL, refUserId: this.config.refUserId, header: this.config.requestHeader } }); this._sessionMap[session.sessionData.sessionId] = { ...existSessionModel, task }; task.startReceiveMsg(); return; } // 没有开 session this._unSavedCallback.push(config); } /** * * @param sessionId sessionId * @returns */ startReceiveMsg(sessionId: string): Promise { const session = this.getSessionWithSessionId(sessionId); if (!session) { this._log(LogType.error, '[manager] session is not exist'); return Promise.resolve(); } return session.startReceiveMsg(); } /** * 对外暴露,用于关闭 session,现在 session 不是一次性的,所以没有关闭的必要了。 * @param sessionId * @returns */ closeSession(sessionId: string): void { const session = this.getSessionWithSessionId(sessionId); if (!session) { return; } // 先删除本地,再关闭 session,防止本地重新用这个 sessionId 进行业务处理。 delete this._sessionMap[sessionId]; } /** * 发送消息 * @param data 发送的消息,以及往某个特定地方发送的 sessionId * @returns */ sendMessage(data: { sessionId: string; msg: SessionSendModel; }): Promise { const sessionId = data.sessionId; const session = this.getSessionWithSessionId(sessionId); // 如果没有启动,那么清空重试次数,重新尝试启动 WS if (!this._socket.isOpen()) { this._socket.clearRetryCount(); this._socket.build().catch((err) => { console.log(err.message); }); } if (session) { return session.sendMsg(data.msg, this._socket.wsId); } return Promise.reject('[manager] session is not exist'); } /** * 获得当前 session 某条消息之前的历史记录 * @param sessionId sessionId * @param cursorMessageId message 的游标Id,一般是最近收到的一条消息,如果没有,那么获取 session 完整的信息 */ fetchSessionHistory( sessionId: string, seqId?: string ): Promise { // 没有 sse 则返回空,因为 WS 不支持历史消息 const session = this.getSessionWithSessionId(sessionId); if (session) { return session.fetchSessionMsg({ history: true, seqId }); } return Promise.reject('[manager] session is not exist'); } /** * * @param session 拉取 task 历史记录的 sessionId * @param seqId 对应的 seqId * @returns */ fetchTaskHistory( session: string, seqId?: string ): Promise { const task = this.getTaskWithSessionId(session); if (task) { return task.fetchTaskMsg({ history: true, seqId }); } return Promise.reject('[manager] task is not exist'); } /** * 用于创建 session * @param mindId 心识 Id * @param mindType 心识类型 * @param mode 模式 * @param callback session 消息回调 * @returns */ private _createSession = ( data: CreateSessionProps ): Promise => { const { // mindId, // mindType, // mode, sourceMessageId, // callback, openNewSession, originSessionId, openCanvasUrl, extParams = {} } = data; return new Promise((resolve, reject) => { const session = this._createPrivateSession(data); const startTime = new Date(); let confirmStart = new Date(); session .openSession({ sourceMessageId, openNewSession, sessionId: originSessionId, openCanvasUrl, extParams }) .then(({ sessionId }) => { const sessionModel = this.getSessionWithSessionId(sessionId); this._sessionMap[sessionId] = { ...sessionModel, session }; confirmStart = new Date(); const endTime = new Date(); const openSessionTime = endTime.getTime() - startTime.getTime(); const confirmSessionTime = endTime.getTime() - confirmStart.getTime(); resolve({ session, openSessionTime, confirmSessionTime }); }) .catch((error) => { reject(error); }); }); }; /** * 创建session类实例 */ private _createPrivateSession(data: CreateSessionProps): Session { const { mindId, mindType, mode, callback } = data; const config = { mindId: mindId, mindType, mode, deviceId: this.config.sessionConnectConfig.deviceId, config: this.config }; const lifeCycle: SessionLifeCycleModel = { sessionClose: (sessionId?: string) => { if (sessionId) { this.closeSession(sessionId); } }, log: this._log }; const sessionConfig: SessionConfig = { baseUrl: this.config.baseURL, header: this.config.requestHeader, config: config, deviceId: this.config.sessionConnectConfig.deviceId, refUserId: this.config.refUserId, urlConfigModel: this.config.sessionURLConfig, lifeCycle: lifeCycle, callback: callback }; return new Session(sessionConfig); } /** * socket 开启的回调处理,主要是断线重连后对已有session的处理 */ private _socketOpenCallback = (_ev: Event, _webSocket?: WebSocket) => { this._log(LogType.warning, '[manager] socket open callback'); if (!this._socket.isChangingSocket) { const sessionIds = Object.keys(this._sessionMap); sessionIds.forEach((sessionId) => { const session = this.getSessionWithSessionId(sessionId); const task = this.getTaskWithSessionId(sessionId); session?.startReceiveMsg(); task?.startReceiveMsg(); }); } }; /** * socket 关闭的回调处理,主要是断线时对已有session的处理 */ private _socketCloseCallback = (_ev: CloseEvent, _webSocket?: WebSocket) => { this._log(LogType.warning, '[manager] socket close callback'); if (this._socket.isChangingSocket) { const sessionIds = Object.keys(this._sessionMap); sessionIds.forEach((sessionId) => { const session = this.getSessionWithSessionId(sessionId); const task = this.getTaskWithSessionId(sessionId); session?.waitingForStart(); task?.waitingForStart(); }); } }; /** * socket 异常关闭的回调处理,包括错误信息逻辑 */ private _socketErrorCallback = (ev: CloseEvent, _webSocket?: WebSocket) => { // 3002 socket重连次数超过最大次数 if (ev.code === SocketError.EXCEED_MAX_COUNT) { this._log(LogType.error, '[manager] 3002'); const sessionIds = Object.keys(this._sessionMap); sessionIds.forEach((sessionId) => { const session = this.getSessionWithSessionId(sessionId); if (!session) { return; } const errorMsg = { subCode: FRONTEND_ERROR_CODE.WS_CONNECT_ERROR, subCodeReason: 'The network is unstable, please check your network connection.', subCodeActions: [ChatSessionSubCodeAction.OPEN_PAGE] // 由于新的逻辑,基本没有重开 session,所以这里使用 OPEN_PAGE 外部自己定义想要的行为 }; session.appendErrorMsg(errorMsg); }); } }; // -------------- 下面是所有私有方法 ---------------- private getSessionModelWithMindInfo(data: { mindId: string; mindType: WS_MIND_TYPE; mode: MIND_MODE; }) { const { mindId, mindType, mode } = data; const result = Object.values(this._sessionMap).find((item) => { const sessionData = item.session.sessionData; const isSame = sessionData.mindId === mindId && sessionData.mindType === mindType && sessionData.mode === mode; return isSame; }); return result; } private getSessionWithSessionId( sessionId: string ): SessionInterface | undefined { const existKey = Object.keys(this._sessionMap).includes(sessionId); if (existKey) { const sessionModelItem = this._sessionMap[sessionId]; return sessionModelItem.session; } return undefined; } private getTaskWithSessionId(sessionId: string): TaskInterface | undefined { const existKey = Object.keys(this._sessionMap).includes(sessionId); if (existKey) { const sessionModelItem = this._sessionMap[sessionId]; return sessionModelItem.task; } return undefined; } private hasSessionId(sessionId: string): boolean { return Object.keys(this._sessionMap).includes(sessionId); } /** * 日志信息 * @param type log 类型 * @param message log 信息 * @param optionalParams log 信息 */ private _log = ( type: LogType, message?: unknown, ...optionalParams: unknown[] ) => { const timeStamp = new Date().getTime(); const timeStampString = '[' + timeStamp + ']'; if (this._showDebug) { switch (type) { case LogType.info: console.info( '[ClientAccessorSDK]', message, ...optionalParams, timeStampString ); break; case LogType.error: console.error( '[ClientAccessorSDK]', message, ...optionalParams, timeStampString ); break; case LogType.warning: console.warn( '[ClientAccessorSDK]', message, ...optionalParams, timeStampString ); break; } } }; }