/* eslint-disable no-param-reassign */ import type { RequestHeader, IOpenSessionParam } from './interface/common'; import { LogType } from './interface/common'; import { type CreateSessionSentence, type ErrorMsg, type OpenSessionResult, type SessionConfig, type SessionDataInfo, type SessionLifeCycleModel, type SessionMessageCallbackObject, type SessionMessageModel, type SessionSendMessageModel, type SessionSendModel, type SessionURLConfig, type SessionInterface, SENDER_TYPE, WS_MSG_DATA_TYPE, SessionMessageType } from './interface/session'; import request from './request'; import { v4 as uuid } from 'uuid'; /** * 用于管理 Session * * @export * @class Session */ export default class Session implements SessionInterface { /** * session 的消息列表,仅包含 session 相关的信息 * * @private * @type {SessionMessageModel[]} * @memberof Session */ private _msgList: SessionMessageModel[] = []; /** * 在第一条消息收到之后,ws 会在我拉取到消息之前发消息过来, * 所以我需要通过第一条消息的 seqId 获取历史消息,在获取完成前,将 isSorting 设置为 true * 然后将 history 的消息进行排序,然后逐条 append 到 msgList 里面,然后通知外面更新 * sort 完成后,将 isSorting 更新成为 false,这个时候将 ws cache 的数据 append 到当前的 msgList 当中, * 如果重复,那么不添加,如果不重复,那么添加,然后通知外面更新。 * * OpenSession(包含 create 和 openLastSession) 之后 isSorting 设置为 true * 直到外面初始化完 sessionId 之后,才开始收消息 */ private _isSorting = false; /** * 在 sorting 过程中缓存的 ws 消息 */ private _sortingCacheWSList: SessionMessageModel[] = []; /** * sessionId,用于标识 session * * @private * @type {string} * @memberof Session */ private _sessionId?: string; /** * session 配置信息 * * @private * @type {SessionConfig} * @memberof Session */ private _config: SessionConfig; /** * session 的用户第一句话。我理解这里后端设计有问题。Sentence 不应该和 session 耦合 * * @private * @type {CreateSessionSentence} * @memberof Session */ private _sentence?: CreateSessionSentence; /** * session 基本信息 * * @readonly * @type {SessionDataInfo} * @memberof Session */ get sessionData(): SessionDataInfo { return { mindId: this._config.config.mindId, mindType: this._config.config.mindType, mode: this._config.config.mode, sessionId: this._sessionId ?? '', sentence: this._sentence }; } /** * session 链接的 baseUrl * * @readonly * @private * @type {string} * @memberof Session */ private get _baseUrl(): string { return this._config.baseUrl; } /** * session 链接的 请求头 * * @readonly * @private * @type {RequestHeader} * @memberof Session */ private get _requestHeader(): RequestHeader { return this._config.header; } /** * session 链接的 url 配置 * * @readonly * @private * @type {SessionURLConfig} * @memberof Session */ private get _sessionPath(): SessionURLConfig { return this._config.urlConfigModel; } /** * 用户 唯一标识,方便内部使用 * * @readonly * @private * @type {string} * @memberof Session */ private get _refUserId(): string { return this._config.refUserId; } /** * session 的生命周期 * * @readonly * @private * @type {SessionLifeCycleModel} * @memberof Session */ private get _lifeCycle(): SessionLifeCycleModel { return this._config.lifeCycle; } /** * session 的回调函数,主要用于处理消息 * * @readonly * @private * @type {SessionMessageCallbackObject} * @memberof Session */ private get _callback(): SessionMessageCallbackObject { return this._config.callback; } /** * 修改 callback */ set callback(callback: SessionMessageCallbackObject) { this._config.callback = callback; } constructor(config: SessionConfig) { this._config = config; this.waitingForStart(); } /** * 开启 session,如果有 sourceMessageId 或者外部告知到那步需要开启新会话,那么走开启会话的逻辑。 * @param openParams 开启的参数 * @returns */ openSession = (openParams: IOpenSessionParam): Promise => { // 有 messageId 说明当前的链路走 chat,进来的时候需要重开 session if (openParams.sourceMessageId || openParams.openNewSession || openParams.openCanvasUrl) { return this._openNewSession(openParams); } return this._fetchLastMessage(openParams.sessionId).then( (historyMsgList) => { const firstMsg = historyMsgList.at(0); const sessionId = firstMsg?.sessionId; if (sessionId) { this._sessionId = sessionId; // 开场的第一个 session 启动后进行展示 historyMsgList.forEach((msg) => { this.appendMsg(msg); }); // 这个时候重新拉取下后端的信息,保证数据完整。 return Promise.resolve({ sessionId }); } return this._openNewSession(openParams); } ); }; /** * 开发服务,根据sessionId查询历史聊天消息 */ fetchHistoryMessage = (sessionId: string | undefined) => { return this._fetchLastMessage(sessionId); } /** * 开发服务,根据sessionId查询历史聊天消息 */ openNewSession = (openParams: IOpenSessionParam) => { return this._openNewSession(openParams); } /** * 初始化完成所有的消息,等到外部告诉,可以接受所有数据了 */ waitingForStart(): void { this._isSorting = true; } /** * 外部通知内部可以开始发送消息出去了,每次初始化的时候将所有消息都吐出去 */ startReceiveMsg = (): Promise => { return this.loadNewMsg().then((msg) => { msg.forEach((item) => { this.appendMsg(item); }); this._sortingCacheWSList.forEach((item) => { this.appendMsg(item); }); this._isSorting = false; this._sortingCacheWSList = []; this._msgReceivedCallback(); }); }; /** * 开启新会话 * @param openParams 会话参数 * @returns */ private _openNewSession = ( openParams: IOpenSessionParam ): Promise => { return this._openSession(openParams).then((newSessionId) => { return Promise.resolve({ sessionId: newSessionId }); }); }; /** * 请求最近的一个 session 的 message 和 sessionId * @returns {Promise} */ private _fetchLastMessage = ( sessionId?: string ): Promise => { const refUserId = this._config.refUserId; const mindId = this._config.config.mindId; if (!mindId || !refUserId) { return Promise.resolve([]); } const data = new URLSearchParams(); data.append('refUserId', refUserId); data.append('mindId', mindId); !!sessionId && data.append('sessionId', sessionId); const path = `${this._config.urlConfigModel.fetchLastMsgPath }?${data.toString()}`; return request({ baseUrl: this._baseUrl, url: path, header: this._requestHeader, method: 'GET' }).then((res) => { return res; }); }; /** * 开启 session。 * * @private * @return {*} {Promise} * @memberof Session */ private _openSession(openParam?: IOpenSessionParam): Promise { const mindId = this._config.config.mindId; const mindType = this._config.config.mindType; const mode = this._config.config.mode; const sourceMessageId = openParam?.sourceMessageId; const openCanvasUrl = openParam?.openCanvasUrl; const path = this._sessionPath.openPath; const extParams = openParam?.extParams || {}; const data = { ...extParams, refUserId: this._refUserId, deviceId: this._config.deviceId, sourceMessageId: sourceMessageId || undefined, openCanvasUrl: openCanvasUrl || undefined, mindId, mindType, mode }; return request< unknown, { sessionId?: string; sentence?: CreateSessionSentence } >({ baseUrl: this._baseUrl, url: path, header: this._requestHeader, method: 'POST', data }).then((res) => { const sessionId: string | undefined = res.sessionId; if (!sessionId) { throw new Error( '[session] request response error, reason: session id is empty' ); } this._lifeCycle.log?.( LogType.info, '[session] create session success: ', sessionId ); this._sessionId = sessionId; this._sentence = res.sentence; return sessionId; }); } /** * 这里直接发送消息出去,在 socket 的回调里面添加信息进来 * @param msg 发送消息 * @returns */ sendMsg(msg: SessionSendModel, wsId: string): Promise { if (!this._sessionId) { return Promise.reject(new Error('[session] session is not open')); } const msgData: SessionSendMessageModel = { ...msg, type: SessionMessageType.msg, sessionId: this._sessionId, refUserId: this._refUserId, wsId: wsId }; this._lifeCycle.log?.(LogType.info, '[session] sendMsg: ', msgData); const timestamp = Date.now(); const messageId = `${uuid()}_____${timestamp}`; const sendData = { timestamp: timestamp, seqId: messageId, messageId, sender: SENDER_TYPE.client, mindId: this._config.config.mindId, index: 0, multipleData: [], ...msgData }; // SSE 每个 session 自己处理自己的消息 return this._asyncSendMsg(sendData).then(() => { // 将消息添加到队列中,然后异步请求出去 this.appendMsg(sendData as SessionMessageModel); }); } /** * 收到 socket 的消息的处理 * @param data 收到的消息 */ receiveMsgFromSocket = (data: SessionMessageModel): void => { if (this._isSorting) { this._sortingCacheWSList.push(data); } else { this.appendMsg(data); } }; /** * 外部添加信息进来 * @param data 收到新的数据 * @returns */ appendMsg = (data: SessionMessageModel): void => { this._config.log?.(LogType.info, '[session] appendMsg with msg: ', data); if (this._sessionId !== data.sessionId) { return; } if (this._isMsgExist(data)) { return; } if (data.subCode) { this.appendErrorMsg(data as ErrorMsg); return; } if (data.type === SessionMessageType.msg) { // 这里 append 放在后面是因为 append 之后会触发一次 ack,这么放可以少一次 ack this._appendMsgList(data); } if (data.type === SessionMessageType.hint) { this._appendHintMsg(data); } if (data.type === SessionMessageType.recommend) { this._appendRecommendMsg(data); } }; /** * * @returns 加载本地消息最后的一条之后的消息,用于消息对齐 */ loadNewMsg = (): Promise => { const loadMsg: ( msgList?: SessionMessageModel[] ) => Promise = (msgList) => { return this.fetchSessionMsg({ history: false, msgList }).then((msg) => { const newMsg = [...(msgList ?? []), ...msg]; // 如果加载新消息的时候,没有新消息,那么就不需要再次加载了。 if (msg.length === 0) { return Promise.resolve(newMsg); } return loadMsg(newMsg); }); }; return loadMsg(); }; /** * 外部直接添加错误消息 * @param data 错误信息 * @returns */ appendErrorMsg = (data: ErrorMsg): void => { this._lifeCycle.log?.(LogType.error, '[session] receive ErrorMsg: ', data); this._callback.onErrorListener?.(data); }; /** * 获取消息,可能是历史,可能是新的消息,根据 history 字段判断,offset 获得拉取数目(offset 之后,基于 seqId 拉取,根据 MessageId 补齐所有内容) * @param data 获得信息 * @param seqId 拉取的 seqId */ fetchSessionMsg = (data: { history: boolean; msgList?: SessionMessageModel[]; seqId?: string; }): Promise => { const { history, msgList, seqId: lastSeqId } = data; const sessionId = this._sessionId; const path = this._config.urlConfigModel.fetchMsgPath; const targetMsg = msgList ?? this._msgList; const fetchLastSeqId = lastSeqId ?? (history ? targetMsg.at(0)?.seqId : targetMsg.at(targetMsg.length - 1)?.seqId); let valid = false; // 拉取历史必须要 seqId,拉取新消息可以不用 seqId if ((history && fetchLastSeqId !== undefined) || !history) { valid = true; } if (!sessionId || !valid) { return Promise.resolve([]); } const direction = history ? 'backward' : 'forward'; const params = new URLSearchParams({ sessionId: sessionId, direction }); if (fetchLastSeqId) { params.append('seqId', fetchLastSeqId); } return request({ baseUrl: this._baseUrl, url: path + '?' + params.toString(), header: this._requestHeader, method: 'GET' }).then((res: SessionMessageModel[]) => { return Promise.resolve(res); }); }; private _asyncSendMsg = (msg: SessionSendMessageModel): Promise => { this._config.log?.(LogType.info, '[session] asyncSendMsg: ', msg); const path = this._sessionPath.sendMsgPath; return request({ baseUrl: this._baseUrl, url: path, header: this._requestHeader, method: 'POST', data: msg }).then(() => { return Promise.resolve(); }); }; /** * 追加消息到消息列表 * * @private * @param {SessionMessageModel} msg * @memberof Session */ private _appendMsgList = (msg: SessionMessageModel): void => { if (msg.type !== SessionMessageType.msg) { return; } switch (msg.sender) { case SENDER_TYPE.client: this._appendSendMessage(msg); break; case SENDER_TYPE.umm: this._appendReceivedMsg(msg); break; default: this._config.log?.( LogType.error, '[session] unknown message type: ', msg ); } }; private _appendHintMsg = (msg: SessionMessageModel): void => { if (msg.type !== SessionMessageType.hint) { return; } this._msgList.push(msg); this._msgReceivedCallback(); }; private _appendRecommendMsg = (msg: SessionMessageModel): void => { if (msg.type !== SessionMessageType.recommend) { return; } this._msgList.push(msg); this._msgReceivedCallback(); }; // - 发消息 private _appendSendMessage(msg: SessionMessageModel): void { this._msgList.push(msg); this._msgReceivedCallback(); } // - 收消息 private _appendReceivedMsg(msg: SessionMessageModel): void { this._msgList.push(msg); this._msgReceivedCallback(); } private _isMsgExist = (msg: SessionMessageModel) => { return this._msgList.some((item) => { return ( item.seqId === msg.seqId && this._sessionId === msg.sessionId && item.type === msg.type && item.messageId === msg.messageId ); }); }; private _msgReceivedCallback = () => { if (!this._isSorting) { this._callback.onMsgUpdateListener?.(this._msgList); } }; }