当前位置: 首页 > news >正文

集成RocketChat至现有的.Net项目中,为ChatGPT铺路

文章目录

    • 前言
    • 项目搭建
      • 后端
      • 前端
    • 代理账号
    • 鉴权方式介绍
    • 登录校验模块
      • 前端鉴权方式
    • 后端鉴权方式
    • 登录委托
    • 使用登录委托
    • 处理聊天消息
      • 前端鉴权方式
      • 后端校验方式
    • 项目地址

前言

今天我们来聊一聊一个Paas的方案,如何集成到一个既有的项目中。
以其中一个需求为例子:在产品项目中,加入IM(即时通信)功能,开始徒手撸代码,会发现工作量很大,去github找开源项目,结果也可能事与愿违:功能不够强大,或者用不同的语言编写的,编译出来程序集无法集成到项目中。
可能当下最好的方案是利用独立的聊天功能组件,作为项目的中间件(Paas方案)。

  1. 组件是独立部署,独立运行的,功能的稳定性,搭建速度快,
  2. 作为基础设施服务,可以用在其他项目中,并且项目中的对接作为抽象层,可随时替换现有组件。

这个聊天组件就是RocketChat。
RocketChat 是一款免费,开源的聊天软件平台。
其主要功能是:群组聊天、相互通信、私密聊群、桌面通知、文件上传、语音/视频、截图等,实现了用户之间的实时消息转换。
https://github.com/RocketChat/Rocket.Chat

它本身是使用Meteor全栈框架以JavaScript开发的Web聊天服务器。本身带有一个精美的web端,甚至有开源的App端。
集成到一个既有的项目中我们是需要做减法的,然而在实际对接中,我们仍然需要解决一些问题:
首先是Rocket.Chat自己有一套独立的用户系统,其中登录鉴权逻辑,这一部分是我们不需要的。
第二是Rocket.Chat聊天功能依赖这个用户系统,需要简化流程同步用户信息,只保留用户,不需要权限,角色。

准备工作:搭建Rocket.Chat服务

Rocket.Chat有两套Api,一个是基于https的REST Api,和一个基于wss的Realtime API, https://developer.rocket.chat/reference/api/realtime-api
这两个Api都需要鉴权。

解决这个有两套方案,一个是通过完全的后端接管,两个Api都经过后端项目进行转发,另一个是后端只接管REST Api, Realtime API和Rocket.Chat服务直接通信

项目搭建

后端

新建一个.Net 6 Abp项目后,添加AbpBoilerplate.RocketChat库,AbpBoilerplate.RocketChat的具体介绍请参考https://blog.csdn.net/jevonsflash/article/details/128342430

dotnet add package AbpBoilerplate.RocketChat

在Domain层中创建IM项目,创建Authorization目录存放与IM鉴权相关的代码,ImWebSocket目录用于存放处理Realtime API相关的代码.

在搭建Rocket.Chat环节,还记得有一个设置管理员的步骤吗?在AdminUserName和AdminPassword配置中,指定这个管理员的密码,

管理员用于在用户未登录时,提供操作的权限主体,

  "Im": {"Provider": "RocketChat","Address": "http://localhost:3000/","WebSocketAddress": "ws://localhost:3000/","AdminUserName": "super","AdminPassword": "123qwe","DefaultPassword": "123qwe"}

前端

用vue2来搭建一个简单的前端界面,需要用到以下库

  • element-UI库
  • axios
  • vuex
  • signalr
    新建一个vue项目,在package.json中的 "dependencies"添加如下:
"axios": "^0.26.1",
"element-ui": "^2.15.6",
"@microsoft/signalr": "^5.0.6"
"vuex": "^3.6.2"

代理账号

代理账号是一个管理员账号
在程序的启动时,要登录这个管理员账号,并保存Token,程序停止时退出登录这个账号。
我们需要一个cache存储管理员账号的登录信息(用户ID和Token)
在Threads目录下创建ImAdminAgentAuthBackgroundWorker,
并在ImModule中注册这个后台任务

private async Task LoginAdminAgent()
{var userName = rocketChatConfiguration.AdminUserName;var password = rocketChatConfiguration.AdminPassword;var loginResult = await imManager.Authenticate(userName, password);if (loginResult.Success && loginResult.Content != null){var cache = imAdminAgentCache.GetCache("ImAdminAgent");await cache.SetAsync("UserId", loginResult.Content.Data.UserId);await cache.SetAsync("AuthToken", loginResult.Content.Data.AuthToken);await cache.SetAsync("UserName", userName);}else{throw new UserFriendlyException("无法登录IM服务Admin代理账号");}
}public override async void Stop()
{base.Stop();var cache = imAdminAgentCache.GetCache("ImAdminAgent");var token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });var userId = (string)cache.Get("UserId", (i) => { return string.Empty; });if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(userId)){return;}using (_iocManager.IocContainer.BeginScope()) //extension method{_iocManager.Resolve<SessionContextDto>().Token = token;_iocManager.Resolve<SessionContextDto>().UserId = userId;_iocManager.Resolve<SessionContextDto>().IsAuthorized = true;try{await imManager.Logout();}catch (Exception ex){throw;}}}

SessionContextDto是一个会话上下文对象,在.net项目中,登录校验成功后写入,在请求Rocket.Chat的时候读取,并写入到请求头中。

在ImModule的PostInitialize方法中注册ImAdminAgentAuthBackgroundWorker

public override void PostInitialize()
{var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();workerManager.Add(IocManager.Resolve<ImAdminAgentAuthBackgroundWorker>());
}

用户登录时,需要传用户名密码,用户名是跟.net项目中相同的,密码可以独立设置,也可以设定约定一个默认密码,那么新建用户和登录的时候,可以不用传密码,直接使用默认密码即可,用户成功登录后,将用户ID和Token回传给前端。

定义传输对象类AuthenticateResultDto

public class AuthenticateResultDto
{public string AccessToken { get; set; }public string UserId { get; set; }
}

在应用层中创建类ImAppService,创建应用层服务Authenticate,用于用户登录。

 private async Task<AuthenticateResultDto> Authenticate(MatoAppSample.Authorization.Users.User user, string password = null)
{var loginResult = await _imManager.Authenticate(user.UserName, password);if (loginResult.Success){var userId = loginResult.Content.Data.UserId;var token = loginResult.Content.Data.AuthToken;this.imAuthTokenCache.Set(user.UserName, new ImAuthTokenCacheItem(userId, token), new TimeSpan(1, 0, 0));}else{this.imAuthTokenCache.Remove(user.UserName);throw new UserFriendlyException($"登录失败, {loginResult.Error}");}return new AuthenticateResultDto{AccessToken = loginResult.Content.Data.AuthToken,UserId = loginResult.Content.Data.UserId};
}

鉴权方式介绍

由于Rocket.Chat的Realtime API基于REST API基础上进行鉴权,在调用完成/api/v1/login接口后,需要在已经建立的Websocket连接中发送

{"msg": "method","method": "login","id": "42","params":[{ "resume": "auth-token" }]
}

详见官方文档

在集成RocketChat时,对于Realtime API方案有二:

  1. 前端鉴权,前端通过Abp登录后,调用/api/v1/login接口,返回token之后存入前端Token缓存中,之后前端将与Rocketchat直接建立websocket联系,订阅的聊天消息和房间消息将被直接推送至前端。

    优点是消息订阅推送直接,效率较高,但前端需要同时顾及Abp的鉴权和RocketChat Realtime API鉴权,前端的代码逻辑复杂,代理账号逻辑复杂,后期扩展性差。小型项目适合此方式

  2. 后端鉴权,前端通过Abp登录后,调用/api/v1/login接口,返回token之后存入后端Token缓存中,由后端发起websocket连接,订阅的聊天消息和房间消息将被转发成signalR消息发送给前端,由后端缓存过期机制统一管理各连接的生命周期。

    优点是统一了前端的消息推送机制,架构更趋于合理,对于多用户端的大型项目,能够减少前端不必要的代码逻辑。但是后端的代码会复杂一些。适合中大型项目。

Realtime API 的前端鉴权

在这里插入图片描述
Realtime API 的后端鉴权

在这里插入图片描述

登录校验模块

前端鉴权方式

由于是从小程序,或者web端共用的所以要分别从Header和Cookie中获取登录信息,IHttpContextAccessor类型的参数用于从http请求上下文对象中访问Header或Cookie,

整个流程如下:

在这里插入图片描述

创建AuthorizedFrontendWrapper.cs,新建AuthorizationVerification方法,此方法是登录校验逻辑

private static void AuthorizationVerification(IHttpContextAccessor _httpContextAccessor, bool useAdminIfNotAuthorized, out StringValues? token, out StringValues? userId)
{var isCommonUserLoginPassed = true;token = _httpContextAccessor.HttpContext?.Request.Headers["X-Auth-Token"];userId = _httpContextAccessor.HttpContext?.Request.Headers["X-User-Id"];if (!ValidateToken(token, userId)){token = _httpContextAccessor.HttpContext?.Request.Cookies["chat_token"];userId = _httpContextAccessor.HttpContext?.Request.Cookies["chat_uid"];if (!ValidateToken(token, userId)){isCommonUserLoginPassed = false;}}var cache = Manager.GetCache("ImAdminAgent");if (!isCommonUserLoginPassed){if (useAdminIfNotAuthorized){//若不存在则取admin作为主体token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });userId = (string)cache.Get("UserId", (i) => { return string.Empty; });if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 当前用户未登录,且初始代理用户未登录");}}else{throw new UserFriendlyException("操作未取得IM服务授权, 当前用户未登录");}}else{if ((string)cache.Get("UserId", (i) => { return string.Empty; }) == userId.Value){token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 初始代理用户未登录");}}}
}

后端鉴权方式

整个流程如下:

在这里插入图片描述

创建AuthorizedBackendWrapper.cs,新建AuthorizationVerification方法,登录校验代码如下

public void AuthorizationVerification(out string token, out string userId)
{User user = null;try{user = userManager.FindByIdAsync(abpSession.GetUserId().ToString()).Result;}catch (Exception){}var userName = user != null ? user.UserName : rocketChatConfiguration.AdminUserName;var password = user != null ? ImUserDefaultPassword : rocketChatConfiguration.AdminPassword;var userIdAndToken = imAuthTokenCache.Get(userName, (i) => { return default; });if (userIdAndToken == default){var loginResult = imManager.Authenticate(userName, password).Result;if (loginResult.Success && loginResult.Content != null){userId = loginResult.Content.Data.UserId;token = loginResult.Content.Data.AuthToken;var imAuthTokenCacheItem = new ImAuthTokenCacheItem(userId, token);imAuthTokenCache.Set(userName, imAuthTokenCacheItem, new TimeSpan(1, 0, 0));var userIdentifier = abpSession.ToUserIdentifier();if (userIdentifier != null){Task.Run(async () =>{await Login(imAuthTokenCacheItem, userIdentifier, userName);});}}else{var adminUserName = rocketChatConfiguration.AdminUserName;var adminPassword = rocketChatConfiguration.AdminPassword;var adminLoginResult = imManager.Authenticate(adminUserName, adminPassword).Result;if (adminLoginResult.Success && adminLoginResult.Content != null){userId = adminLoginResult.Content.Data.UserId;token = adminLoginResult.Content.Data.AuthToken;if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 无法登录账号" + userName);}}else{throw new UserFriendlyException("账号登录失败:" + adminLoginResult.Error);}}}else{userId = userIdAndToken.UserId;token = userIdAndToken.Token;}if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 登录失败");}
}

登录委托

在AuthorizedFrontendWrapper(或AuthorizedBackendWrapper)中

写一个登录委托AuthorizedChatAction,用于包装一个需要登录之后才能使用的操作

public static async Task AuthorizedChatAction(Func<Task> func, IocManager _iocManager)
{if (_iocManager.IsRegistered<SessionContextDto>()){string token, userId;AuthorizationVerification(out token, out userId);using (_iocManager.IocContainer.Begin()) //extension method{_iocManager.Resolve<SessionContextDto>().Token = token;_iocManager.Resolve<SessionContextDto>().UserId = userId;_iocManager.Resolve<SessionContextDto>().IsAuthorized = true;try{await func();}catch (Exception ex){throw;}}}else{throw new UserFriendlyException("没有注册即时通信会话上下文对象");}
}

使用登录委托

我们在创建IM相关方法的时候,需要用AuthorizedFrontendWrapper(或AuthorizedBackendWrapper),来包装登录校验的逻辑。

public async Task<bool> DeleteUser(long userId)
{var user = await _userManager.GetUserByIdAsync(userId);var result = await AuthorizedBackendWrapper.AuthorizedChatAction(() =>{return _imManager.DeleteUser(user.UserName);}, _iocManager);if (!result.Success || !result.Content){throw new UserFriendlyException($"删除失败, {result.Error}");}return result.Content;
}

处理聊天消息

前端鉴权方式

新建messageHandler_frontend_auth.ts处理程序

客户端支持WebSocket的浏览器中,在创建socket后,可以通过onopen、onmessage、onclose和onerror四个事件对socket进行响应。

我已经封装好了一个WebSocket 通信模块\web\src\utils\socket.ts,Socket对象是一个WebSocket抽象,后期将扩展到uniapp小程序项目上使用的WebSocket。通过这个对象可以方便的进行操作。

创建一个Socket对象wsConnection,用于接收和发送基于wss的Realtime API消息

const wsRequestUrl: string = "ws://localhost:3000/websocket";const socketOpt: ISocketOption = {server: wsRequestUrl,reconnect: true,reconnectDelay: 2000,
};const wsConnection: Socket = new Socket(socketOpt);

WebSocket的所有操作都是采用事件的方式触发的,这样不会阻塞UI,是的UI有更快的响应时间,有更好的用户体验。

连接建立后,客户端和服务器就可以通过TCP连接直接交换数据。我们订阅onmessage事件触发newMsgHandler处理信息

wsConnection.$on("message", newMsgHandler);

当链接打开后,立即发送{"msg":"connect","version":"1","support":["1","pre2","pre1"]}报文

wsConnection.$on("open", (newMsg) => {console.info("WebSocket Connected");wsConnection.send({msg: "connect",version: "1",support: ["1"],});});

建立链接后,会从Rocket.Chat收到connected消息,此时需要发送登录请求的消息到Rocket.Chat
接收到报文

"{"msg":"connected","session":"cMvzWpCNSCR24bwCf"}"

发送报文

{"msg":"method","method":"login","params":[{"resume":"wY67O8rJFyf2FrqD5vxpQjIUs5tdThmyfW_VaA7MrsG"}],"id":"1"}

接下来,在newMsgHandler方法中,根据msg类型,处理一系列的消息

const newMsgHandler: Function = (newMsgRaw) => {if (!getIsNull(newMsgRaw)) {if (newMsgRaw.msg == "ping") {wsConnection.send({msg: "pong",});} else if (newMsgRaw.msg == "connected") {let newMsg: ConnectedWsDto = newMsgRawlet session = newMsg.session;if (wsConnection.isConnected) {wsConnection.send({msg: "method",method: "login",params: [{resume: UserModule.chatToken,},],id: "1",});}} else if (newMsgRaw.msg == "added") {subEvent("stream-notify-user", "message");subEvent("stream-notify-user", "subscriptions-changed");subEvent("stream-notify-user", "rooms-changed");} else if (newMsgRaw.msg == "changed") {let newMsg: SubChangedWsDto = newMsgRawif (newMsg.collection == "stream-notify-user") {let fields = newMsg.fields;if (fields.eventName.indexOf("/") != -1) {let id = fields.eventName.split('/')[0];let eventName = fields.eventName.split('/')[1];if (eventName == "subscriptions-changed") {let args = fields.args;let msg: ISubscription = null;let method: string;args.forEach((arg) => {if (typeof arg == "string") {if (arg == "remove" || arg == "insert") {method = arg;}}else if (typeof arg == "object") {msg = arg}});$EventBus.$emit("getRoomSubscriptionChangedNotification", { msg, method });}else if (eventName == "rooms-changed") {let args = fields.args;let msg: RoomMessageNotificationDto = null;args.forEach((arg) => {if (typeof arg == "object") {msg = arg}});$EventBus.$emit("getRoomMessageNotification", msg.lastMessage);}}else {let id = fields.eventName}}else if (newMsg.collection == "stream-room-messages") {let fields = newMsg.fields;let id = fields.eventNamelet msg: MessageItemDto = fields.args;$EventBus.$emit("getRoomMessageNotification", msg);}}}
}

store/chat.ts文件中,定义了ChatState用于存储聊天信息,当有消息收到,或者房间信息变更时,更新这些存储对象

export interface IChatState {currentChannel: ChannelDto;channelList: Array<ChannelDto>;currentMessage: MessageDto;
}

后端校验方式

Login时将生成webSocket对象,并发送connect消息

public async Task Login(ImAuthTokenCacheItem imAuthTokenCacheItem, UserIdentifier userIdentifier, string userName)
{using (var webSocket = new ClientWebSocket()){webSocket.Options.RemoteCertificateValidationCallback = delegate { return true; };var url = Flurl.Url.Combine(rocketChatConfiguration.WebSocketHost, "websocket");await webSocket.ConnectAsync(new Uri(url), CancellationToken.None);if (webSocket.State == WebSocketState.Open){var model = new ImWebSocketConnectRequest(){Msg = "connect",Version = "1",Support = new string[] { "1" }};var jsonStr = JsonConvert.SerializeObject(model);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);await Echo(webSocket, imAuthTokenCacheItem, userIdentifier, userName);}}
}

每次接收指令时,将判断缓存中的Token值是否合法,若不存在,或过期(session变化),将主动断开websocket连接
在接收Realtime API消息后,解析方式同前端鉴权逻辑
在拿到数据后,做signalR转发。

private async Task Echo(WebSocket webSocket, ImAuthTokenCacheItem imAuthTokenCacheItem, UserIdentifier userIdentifier, string userName)
{JsonSerializerSettings serializerSettings = new JsonSerializerSettings(){NullValueHandling = NullValueHandling.Ignore};var buffer = new byte[1024 * 4];var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);string session=string.Empty;ImAuthTokenCacheItem im;while (!receiveResult.CloseStatus.HasValue){im = imAuthTokenCache.GetOrDefault(userName);if (im == null){await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,"缓存超时自动退出",CancellationToken.None);Console.WriteLine(userName + "超时主动断开IM连接");break;}else{if (!string.IsNullOrEmpty(session) && im.Session!=session){await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,"缓存更新自动退出",CancellationToken.None);Console.WriteLine(userName + "缓存更新主动断开IM连接");break;}}var text = Encoding.UTF8.GetString(buffer.AsSpan(0, receiveResult.Count));if (!string.IsNullOrEmpty(text)){dynamic response = JsonConvert.DeserializeObject<dynamic>(text);if (response.msg == "ping"){var model = new ImWebSocketCommandRequest(){Msg = "pong",};var jsonStr = JsonConvert.SerializeObject(model, serializerSettings);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);}if (response.msg == "connected"){session = response.session;var model = new ImWebSocketCommandRequest(){Msg = "method",Method = "login",Params = new object[]{new {resume = imAuthTokenCacheItem.Token,}},Id = "1"};imAuthTokenCacheItem.Session = session;imAuthTokenCache.Set(userName, imAuthTokenCacheItem, new TimeSpan(1, 0, 0));var jsonStr = JsonConvert.SerializeObject(model, serializerSettings);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);}else if (response.msg == "added"){await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "message");await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "subscriptions-changed");await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "rooms-changed");}else if (response.msg == "changed"){var newMsg = response;if (newMsg.collection == "stream-notify-user"){var fields = newMsg.fields;var fullEventName = fields.eventName.ToString();if (fullEventName.IndexOf("/") != -1){var id = fullEventName.Split('/')[0];var eventName = fullEventName.Split('/')[1];if (eventName == "subscriptions-changed"){var args = fields.args;dynamic msg = null;var method = string.Empty;foreach (var arg in args as IEnumerable<dynamic>){if (arg.ToString() == "remove" || arg.ToString() == "insert"){method = arg.ToString();}else{msg = arg;}}await signalREventPublisher.PublishAsync(userIdentifier, "getRoomSubscriptionChangedNotification", new { msg, method });}else if (eventName == "rooms-changed"){var args = fields.args;dynamic msg = null;var method = string.Empty;foreach (var arg in args as IEnumerable<dynamic>){if (arg.ToString() == "updated"){method = arg.ToString();}else{msg = arg;}};var jobject = msg.lastMessage as JObject;await signalREventPublisher.PublishAsync(userIdentifier, "getRoomMessageNotification", jobject);}}else{var id = fields.eventName;}}}else if (response.collection == "stream-room-messages"){var fields = response.fields;var id = fields.eventName;var msg = fields.args;var jobject = msg as JObject;await signalREventPublisher.PublishAsync(userIdentifier, "getRoomMessageNotification", jobject);}}try{receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);}catch (Exception ex){Console.WriteLine(userName + "异常断开IM连接");break;}}try{await webSocket.CloseAsync(
receiveResult.CloseStatus.Value,
receiveResult.CloseStatusDescription,
CancellationToken.None);}catch (Exception ex){}imAuthTokenCache.Remove(userName);}private async Task SubEvent(WebSocket webSocket, ImAuthTokenCacheItem imAuthTokenCacheItem, string name, string type)
{var eventstr = $"{imAuthTokenCacheItem.UserId}/${type}";var id = RandomHelper.GetRandom(100000).ToString().PadRight(5, '0');var model = new ImWebSocketCommandRequest(){Msg = "sub",Params = new object[]{eventstr,new {useCollection= false,args = new string[]{ }}},Id = id,Name = name,};var jsonStr = JsonConvert.SerializeObject(model);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);
}

SignalREventPublisher.cs 中的PublishAsync,将消息转发给对应的用户。

public async Task PublishAsync(IUserIdentifier userIdentifier, string method, object message)
{try{var onlineClients = _onlineClientManager.GetAllByUserId(userIdentifier);foreach (var onlineClient in onlineClients){var signalRClient = _hubContext.Clients.Client(onlineClient.ConnectionId);if (signalRClient == null){Logger.Debug("Can not get user " + userIdentifier.ToUserIdentifier() + " with connectionId " + onlineClient.ConnectionId + " from SignalR hub!");continue;}await signalRClient.SendAsync(method, message);}}catch (Exception ex){Logger.Warn("Could not send notification to user: " + userIdentifier.ToUserIdentifier());Logger.Warn(ex.ToString(), ex);}}

前端代码则要简单得多
新建messageHandler_backend_auth.ts处理程序

import * as signalR from "@microsoft/signalr";

创建一个HubConnection对象hubConnection,用于接收SignalR消息

const baseURL = "http://localhost:44311/"; // url = base url + request url
const requestUrl = "signalr";
let header = {};
if (UserModule.token) {header = {"X-XSRF-TOKEN": UserModule.token,Authorization: "Bearer " + UserModule.token,};
}//signalR config
const hubConnection: signalR.HubConnection = new signalR.HubConnectionBuilder().withUrl(baseURL + requestUrl, {headers: header,accessTokenFactory: () => getAccessToken(),transport: signalR.HttpTransportType.WebSockets,logMessageContent: true,logger: signalR.LogLevel.Trace,}).withAutomaticReconnect().withHubProtocol(new signalR.JsonHubProtocol()).build();

我们只需要响应后端程序中定义好的signalR消息的methodName就可以了

hubConnection.on("getRoomMessageNotification", (n: MessageItemDto) => {console.info(n.msg)if (ChatModule.currentChannel._id != n.rid) {ChatModule.increaseChannelUnread(n.rid);} else {if (n.t == null) {n.from =n.u.username == UserModule.userName? constant.MSG_FROM_SELF: constant.MSG_FROM_OPPOSITE;} else {n.from = constant.MSG_FROM_SYSTEM;}ChatModule.appendMessage(n);}
});hubConnection.on("getRoomSubscriptionChangedNotification", (n) => {console.info(n.method, n.msg)if (n.method == "insert") {console.info(n.msg + "has been inserted!");ChatModule.insertChannel(n.msg);}else if (n.method == "update") {}
});

至此,完成了所有的集成工作。

此文目的是介绍一种思路,使用缓存生命周期管理的相关机制,规避第三方用户系统对现有项目的用户系统的影响。举一反三,可以用到其他Paas的方案集成中。最近ChatGPT很火,可惜没时间研究怎么接入,有闲工夫的同学们可以尝试着写一个ChatGPT聊天机器人,欢迎大家评论留言!

最终效果如图
在这里插入图片描述

项目地址

Github:matoapp-samples

相关文章:

集成RocketChat至现有的.Net项目中,为ChatGPT铺路

文章目录前言项目搭建后端前端代理账号鉴权方式介绍登录校验模块前端鉴权方式后端鉴权方式登录委托使用登录委托处理聊天消息前端鉴权方式后端校验方式项目地址前言 今天我们来聊一聊一个Paas的方案&#xff0c;如何集成到一个既有的项目中。 以其中一个需求为例子&#xff1a…...

王道操作系统课代表 - 考研计算机 第三章 内存管理 究极精华总结笔记

本篇博客是考研期间学习王道课程 传送门 的笔记&#xff0c;以及一整年里对 操作系统 知识点的理解的总结。希望对新一届的计算机考研人提供帮助&#xff01;&#xff01;&#xff01; 关于对 “内存管理” 章节知识点总结的十分全面&#xff0c;涵括了《操作系统》课程里的全部…...

Cypher中的聚合

深解Cypher中的聚合 值或计数的聚合要么从查询返回&#xff0c;要么用作多步查询下一部分的输入。查看数据模型 CALL db.schema.visualization() 查看图中节点的属性类型 CALL db.schema.notetypeproperties() 查看图中关系的属性类型 CALL db.schema.reltypeproperties() C…...

图注意网络GAT理解及Pytorch代码实现【PyGAT代码详细注释】

文章目录GAT代码实现【PyGAT】GraphAttentionLayer【一个图注意力层实现】用上面实现的单层网络测试加入Multi-head机制的GAT对数据集Cora的处理csr_matrix()处理稀疏矩阵encode_onehot()对label编号build graph邻接矩阵构造GAT的推广GAT 题&#xff1a;Graph Attention Netwo…...

项目成本管理中的常见误区及解决方案

做过项目的人都明白&#xff0c;项目实施时间一般很长&#xff0c;在实施期间总有很多项目结果不尽人意的问题。要使一个项目取得成功&#xff0c;就要结合很多因素一起才能作用&#xff0c;其中做好项目成本的管理就是最重要的步骤之一&#xff0c;下面列出了常见的项目成本管…...

墨天轮2022年度数据库获奖名单

2022年&#xff0c;国家相继从高位部署、省级试点布局、地市重点深入三个维度&#xff0c;颁布了多项中国数据库行业发展的利好政策。但是我们也能清晰地看到&#xff0c;中国数据库行业发展之路道阻且长&#xff0c;而道路上的“拦路虎”之一则是生态。中国数据库的发展需要多…...

仓储调度|库存管理系统

技术&#xff1a;Java、JSP等摘要&#xff1a;随着电子商务技术和网络技术的快速发展&#xff0c;现代物流技术也在不断进步。物流技术是指与物流要素活动有关的所有专业技术的总称&#xff0c;包括各种操作方法、管理技能等&#xff0c;物流业采用某些现代信息技术方面的成功经…...

Canvas入门-01

导读&#xff1a; 读完全文需要2min。通过这篇文章&#xff0c;你可以了解到以下内容&#xff1a; Canvas标签基本属性如何使用Canvas画矩形、圆形、线条、曲线、笑脸&#x1f60a; 如果你曾经了解过Canvas&#xff0c;可以对照目录回忆一下能否回答上来 毕竟带着问题学习最有效…...

运算符优先级

醋坛酸味罐&#xff0c;位落跳福豆 醋&#xff1a;初等运算符&#xff1a; () [] -> . 坛&#xff1a;单目运算符&#xff1a; - ~ – * & ! sizeof 右结合 酸&#xff1a;算术运算符&#xff1a; - * / % 味&#xff1a;位移运算符&#xff1a;>> << …...

微信小程序使用scss编译wxss文件的配置步骤

文章目录1、在 vscode 中搜索 easysass 插件并安装2、在微信开发工具中导入安装的easysass插件3、修改 spook.easysass-0.0.6/package.json 文件中的配置4、重启开发者工具&#xff0c;就可用使用了微信小程序开发者工具集成了 vscode 编辑器&#xff0c;可以使用 vscode 中众多…...

一步一步教你如何使用 Visual Studio Code 编译一段 C# 代码

以下是一步一步教你如何使用 Visual Studio Code 编写使用 C# 语言输出当前日期和时间的代码&#xff1a; 1、下载并安装 .NET SDK。您可以从 Microsoft 官网下载并安装它。 2、打开 Visual Studio Code&#xff0c;并安装 C# 扩展。您可以在 Visual Studio Code 中通过扩展菜…...

vue-cli中的环境变量注意点

在客户端侧代码中使用环境变量只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们&#xff1a;console.log(process.env.VUE_APP_SECRET)在构建过程中&#xff0c;process.env.VUE_APP_SECRET 将会被相应的值所…...

2.3数据类型

文章目录1. 命名规则2.字符3.数字4.日期5.图片1. 命名规则 字段名必须以字母开头&#xff0c;尽量不要使用拼音长度不能超过30个字符&#xff08;不同数据库&#xff0c;不同版本会有不同&#xff09;不能使用SQL的保留字&#xff0c;如where,order,group只能使用如下字符a-z、…...

Kafka基本概念

什么是Kafka Kafka是一个消息系统。它可以集中收集生产者的消息&#xff0c;并由消费者按需获取。在Kafka中&#xff0c;也将消息称为日志(log)。 一个系统&#xff0c;若仅有一类或者少量的消息&#xff0c;可直接进行发送和接收。 随着业务量日益复杂&#xff0c;消息的种类…...

使用QueryBuilders、NativeSearchQuery实现复杂查询

使用QueryBuilders、NativeSearchQuery实现复杂查询 本文继续前面文章《ElasticSearch系列&#xff08;二&#xff09;springboot中集成使用ElasticSearch的Demo》&#xff0c;在前文中&#xff0c;我们介绍了使用springdata做一些简单查询&#xff0c;但是要实现一些高级的组…...

taobao.open.account.update( Open Account数据更新 )

&#xffe5;开放平台免费API不需用户授权 Open Account数据更新 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 响应参数 点击获取key和secret 请求示例 TaobaoClient client new DefaultTaobaoClient(url, appkey, sec…...

PT100铂电阻温度传感器

PT100温度传感器又叫做铂热电阻。     热电阻是中低温区&#xfe61;常用的一种温度检测器。它的主要特点是测量精度高&#xff0c;性能稳定。其中铂热电阻的测量精确度是&#xfe61;高的&#xff0c;它不仅广泛应用于工业测温&#xff0c;而且被制成标准的基准仪。金属热…...

蓝桥杯-本质上升序列

没有白走的路&#xff0c;每一步都算数&#x1f388;&#x1f388;&#x1f388; 题目描述&#xff1a; 小蓝特别喜欢单调递增的事物 在一个字符串中如果取出若干个字符&#xff0c;按照在原来字符串中的顺序排列在一起&#xff0c;组成的新的字符串如果是单调递增的&#xf…...

synchronized锁重入验证

文章目录synchronized锁重入验证1. 可重入锁2. synchronized锁重入2.1 本类同步方法内部调用本类其它同步方法2.2 子类同步方法内部调用父类的同步方法2.3 A类的同步方法内部调用B类的同步方法3. synchronized修饰方法写法synchronized锁重入验证 1. 可重入锁 可重入锁&#…...

超简单的计数排序!!

假设给定混乱数据为&#xff1a;3&#xff0c;0&#xff0c;1&#xff0c;3&#xff0c;6&#xff0c;5&#xff0c;4&#xff0c;2&#xff0c;1&#xff0c;9。 下面我们将通过使用计数排序的思想来完成对上面数据的排序。(先不谈负数) 计数排序 该排序的思路和它的名字一样…...

RestClient

什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端&#xff0c;它允许HTTP与Elasticsearch 集群通信&#xff0c;而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级&#xff…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…...

stm32G473的flash模式是单bank还是双bank?

今天突然有人stm32G473的flash模式是单bank还是双bank&#xff1f;由于时间太久&#xff0c;我真忘记了。搜搜发现&#xff0c;还真有人和我一样。见下面的链接&#xff1a;https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

大语言模型如何处理长文本?常用文本分割技术详解

为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

spring:实例工厂方法获取bean

spring处理使用静态工厂方法获取bean实例&#xff0c;也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下&#xff1a; 定义实例工厂类&#xff08;Java代码&#xff09;&#xff0c;定义实例工厂&#xff08;xml&#xff09;&#xff0c;定义调用实例工厂&#xff…...

laravel8+vue3.0+element-plus搭建方法

创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...