From d9e91bcbf54553eb9aed33d3ef94d379c13ed8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A9=99=E5=AD=90?= <454313500@qq.com> Date: Sun, 21 Jul 2024 13:37:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E8=81=8A=E5=A4=A9=E6=96=B0=E5=A2=9Eai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dtos/AiChatContextDto.cs | 24 --- .../Services/AiChatService.cs | 25 +-- .../Enums/MessageTypeEnum.cs | 5 +- .../Model/MessageContext.cs | 13 +- .../Managers/AiManager.cs | 62 ++++--- Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj | 3 + Yi.Bbs.Vue3/src/apis/chatMessageApi.js | 8 + .../src/components/ArticleContentInfo.vue | 2 +- Yi.Bbs.Vue3/src/components/MavonEdit.vue | 4 +- Yi.Bbs.Vue3/src/hubs/chatHub.js | 16 +- Yi.Bbs.Vue3/src/layout/AppHeader.vue | 2 +- Yi.Bbs.Vue3/src/stores/chat.js | 14 ++ Yi.Bbs.Vue3/src/utils/icon.js | 8 +- Yi.Bbs.Vue3/src/views/chathub/Index.vue | 159 +++++++++++++----- Yi.Bbs.Vue3/src/views/lucky/Index.vue | 1 - Yi.Bbs.Vue3/src/views/profile/UserAvatar.vue | 2 +- Yi.Bbs.Vue3/src/views/start/Index.vue | 2 +- Yi.Bbs.Vue3/vite.config.js | 2 +- 18 files changed, 240 insertions(+), 112 deletions(-) delete mode 100644 Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Dtos/AiChatContextDto.cs diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Dtos/AiChatContextDto.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Dtos/AiChatContextDto.cs deleted file mode 100644 index 4a337f5a..00000000 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Dtos/AiChatContextDto.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Yi.Framework.ChatHub.Application.Dtos -{ - public class AiChatContextDto - { - public AnswererTypeEnum AnswererType { get; set; } - - public string Message { get; set; } - - - public int Number { get; set; } - } - - public enum AnswererTypeEnum - { - Ai, - User - } -} diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Services/AiChatService.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Services/AiChatService.cs index e499c1b5..31c8ab8e 100644 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Services/AiChatService.cs +++ b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Application/Services/AiChatService.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; using Volo.Abp.Application.Services; using Yi.Framework.ChatHub.Domain.Managers; +using Yi.Framework.ChatHub.Domain.Shared.Dtos; using Yi.Framework.ChatHub.Domain.Shared.Model; -using Yi.Framework.ChatHub.Domain.SignalRHubs; namespace Yi.Framework.ChatHub.Application.Services { @@ -19,14 +14,24 @@ namespace Yi.Framework.ChatHub.Application.Services private readonly UserMessageManager _userMessageManager; public AiChatService(AiManager aiManager, UserMessageManager userMessageManager) { _aiManager = aiManager; _userMessageManager = userMessageManager; } + + /// + /// ai聊天 + /// + /// + /// [Authorize] [HttpPost] - public async Task ChatAsync() + + public async Task ChatAsync([FromBody] List chatContext) { - await foreach (var aiResult in _aiManager.ChatAsStreamAsync()) + var contextId = Guid.NewGuid(); + await foreach (var aiResult in _aiManager.ChatAsStreamAsync(chatContext)) { - await _userMessageManager.SendMessageAsync(MessageContext.CreateAi(aiResult, CurrentUser.Id!.Value)); + await _userMessageManager.SendMessageAsync(MessageContext.CreateAi(aiResult, CurrentUser.Id!.Value, contextId)); } + + await _userMessageManager.SendMessageAsync(MessageContext.CreateAi(null, CurrentUser.Id!.Value, contextId)); } } } diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Enums/MessageTypeEnum.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Enums/MessageTypeEnum.cs index 5a8c558d..ef07ef46 100644 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Enums/MessageTypeEnum.cs +++ b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Enums/MessageTypeEnum.cs @@ -9,10 +9,11 @@ namespace Yi.Framework.ChatHub.Domain.Shared.Enums public enum MessageTypeEnum { - Ai, + Personal, Group, - All + All, + Ai } } diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Model/MessageContext.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Model/MessageContext.cs index a340641e..aace06f6 100644 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Model/MessageContext.cs +++ b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain.Shared/Model/MessageContext.cs @@ -16,9 +16,9 @@ namespace Yi.Framework.ChatHub.Domain.Shared.Model { return context.Where(x => x.ReceiveId is not null).Select(x => x.ReceiveId!.Value).Union(context.Select(x => x.SendUserId)).ToList(); } - public static List MapperUserInfo(this List messageContexts,List userRoleMenuDtos) + public static List MapperUserInfo(this List messageContexts, List userRoleMenuDtos) { - var userInfoDic = userRoleMenuDtos.ToDictionary(x => x.User.Id); + var userInfoDic = userRoleMenuDtos.Select(x => new UserRoleMenuDto { User = x.User }).ToDictionary(x => x.User.Id); foreach (var context in messageContexts) { if (context.ReceiveId is not null) @@ -42,9 +42,9 @@ namespace Yi.Framework.ChatHub.Domain.Shared.Model } - public static MessageContext CreateAi(string content, Guid receiveId) + public static MessageContext CreateAi(string? content, Guid receiveId, Guid id ) { - return new MessageContext() { MessageType = MessageTypeEnum.Ai, Content = content, ReceiveId = receiveId }; + return new MessageContext() { MessageType = MessageTypeEnum.Ai, Content = content??string.Empty, ReceiveId = receiveId, Id = id }; } public static MessageContext CreateAll(string content, Guid sendUserId) @@ -86,6 +86,11 @@ namespace Yi.Framework.ChatHub.Domain.Shared.Model /// 消息内容 /// public string Content { get; set; } + + /// + /// 上下文Id,主要用于ai流式传输 + /// + public Guid Id { get; set; } public DateTime CreationTime { get; protected set; } = DateTime.Now; } diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs index be696e9a..2b760068 100644 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs +++ b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using Microsoft.Extensions.Options; using OpenAI; using OpenAI.Managers; @@ -10,11 +6,11 @@ using OpenAI.ObjectModels; using OpenAI.ObjectModels.RequestModels; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Services; +using Yi.Framework.ChatHub.Domain.Shared.Dtos; using Yi.Framework.ChatHub.Domain.Shared.Options; - namespace Yi.Framework.ChatHub.Domain.Managers { - public class AiManager : DomainService, ISingletonDependency + public class AiManager : ISingletonDependency { public AiManager(IOptions options) { @@ -26,25 +22,49 @@ namespace Yi.Framework.ChatHub.Domain.Managers } private OpenAIService OpenAIService { get; } - public async IAsyncEnumerable ChatAsStreamAsync() + public async IAsyncEnumerable ChatAsStreamAsync(List aiChatContextDtos) { - var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest - { - Messages = new List - { - ChatMessage.FromUser("特朗普是谁?"), - }, - Model = Models.Gpt_4, - }); + var temp = "站长正在接入ChatGpt,敬请期待~"; - await foreach (var result in completionResult) + for (var i = 0; i < temp.Length; i++) { - if (result.Successful) - { - yield return result.Choices.FirstOrDefault()?.Message.Content??string.Empty; - } + await Task.Delay(200); + yield return temp[i].ToString(); } + + + + //if (aiChatContextDtos.Count == 0) + //{ + // yield return null; + //} + + //List messages= aiChatContextDtos.Select(x => + //{ + // if (x.AnswererType == AnswererTypeEnum.Ai) + // { + // return ChatMessage.FromSystem(x.Message); + // } + // else + // { + // return ChatMessage.FromUser(x.Message); + // } + //}).ToList(); + //var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest + //{ + // Messages = messages, + // Model = Models.Gpt_3_5_Turbo + //}); + + //await foreach (var result in completionResult) + //{ + // if (result.Successful) + // { + // yield return result.Choices.FirstOrDefault()?.Message.Content ?? string.Empty; + // } + //} + } } } diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj b/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj index 5b171eb3..e116a610 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj @@ -31,6 +31,9 @@ Always + + Always + Always diff --git a/Yi.Bbs.Vue3/src/apis/chatMessageApi.js b/Yi.Bbs.Vue3/src/apis/chatMessageApi.js index 517dac85..3ed0d791 100644 --- a/Yi.Bbs.Vue3/src/apis/chatMessageApi.js +++ b/Yi.Bbs.Vue3/src/apis/chatMessageApi.js @@ -21,4 +21,12 @@ export function sendGroupMessage(data) { method: "get" }); } + + export function sendAiChat(data) { + return request({ + url: "/ai-chat/chat", + method: "post", + data + }); + } \ No newline at end of file diff --git a/Yi.Bbs.Vue3/src/components/ArticleContentInfo.vue b/Yi.Bbs.Vue3/src/components/ArticleContentInfo.vue index c880cc4d..93c8665c 100644 --- a/Yi.Bbs.Vue3/src/components/ArticleContentInfo.vue +++ b/Yi.Bbs.Vue3/src/components/ArticleContentInfo.vue @@ -117,7 +117,7 @@ const codeHandler = (code, language) => { // 将代码包裹在 textarea 中,由于防止textarea渲染出现问题,这里将 "<" 用 "<" 代替,不影响复制功能 let html = `
${language}复制代码
${preCode}
`; codeCopyDic.push({id: codeIndex,code:code}); - console.log(codeCopyDic.length); + // console.log(codeCopyDic.length); return html; // } catch (error) { diff --git a/Yi.Bbs.Vue3/src/components/MavonEdit.vue b/Yi.Bbs.Vue3/src/components/MavonEdit.vue index 819f3cd0..fee13136 100644 --- a/Yi.Bbs.Vue3/src/components/MavonEdit.vue +++ b/Yi.Bbs.Vue3/src/components/MavonEdit.vue @@ -118,14 +118,14 @@ const imgAdd = async (pos, $file) => { formdata.append('file', $file); const response = await upload(formdata) const url = `${import.meta.env.VITE_APP_BASEAPI}/file/${response.data[0].id}/true`; - console.log(url) + //console.log(url) md.value.$img2Url(pos, url); } //选择表情包 const onSelectEmoji=(emoji)=>{ - console.log(emoji.i,"emoji"); + //console.log(emoji.i,"emoji"); text.value+=emoji.i } diff --git a/Yi.Bbs.Vue3/src/hubs/chatHub.js b/Yi.Bbs.Vue3/src/hubs/chatHub.js index 2441973f..c2aaa819 100644 --- a/Yi.Bbs.Vue3/src/hubs/chatHub.js +++ b/Yi.Bbs.Vue3/src/hubs/chatHub.js @@ -13,17 +13,27 @@ const receiveMsg = (connection) => { }); //接受其他用户消息 connection.on("receiveMsg", (type, content) => { - chatStore.addMsg(content); + const letChatStore = useChatStore(); + //如果是ai消息,还要进行流式显示 + // alert(type) + if (type == 3) { + letChatStore.addOrUpdateMsg(content); + } + else { + letChatStore.addMsg(content); + } + + }); //用户状态-正在输入中,无 connection.on("userStatus", (type) => { }); }; -export function start(){ +export function start() { signalR.start(`chat`, receiveMsg); } -export function close(){ +export function close() { signalR.SR.stop(); } diff --git a/Yi.Bbs.Vue3/src/layout/AppHeader.vue b/Yi.Bbs.Vue3/src/layout/AppHeader.vue index a063c692..b9d9d06b 100644 --- a/Yi.Bbs.Vue3/src/layout/AppHeader.vue +++ b/Yi.Bbs.Vue3/src/layout/AppHeader.vue @@ -155,7 +155,7 @@ const fetchNoticeData = async () => { const handleSelect = (key, keyPath) => { - console.log(key, keyPath); + //console.log(key, keyPath); }; const logout = async () => { ElMessageBox.confirm(`确定登出系统吗?`, "警告", { diff --git a/Yi.Bbs.Vue3/src/stores/chat.js b/Yi.Bbs.Vue3/src/stores/chat.js index 803a3dbf..c7d88325 100644 --- a/Yi.Bbs.Vue3/src/stores/chat.js +++ b/Yi.Bbs.Vue3/src/stores/chat.js @@ -7,9 +7,23 @@ const chatStore = defineStore("chat", { getters: { allMsgContext: (state) => state.msgList.filter(x=>x.messageType=="All"), personalMsgContext: (state) => state.msgList.filter(x=>x.messageType=="Personal"), + aiMsgContext: (state) => state.msgList.filter(x=>x.messageType=="Ai") }, actions: { + addOrUpdateMsg(msg){ + var currentMsg= this.msgList.filter(x => x.id == msg.id)[0]; + //当前没有包含,如果有相同的上下文id,只需要改变content即可 + if(currentMsg==undefined) + { + this.addMsg(msg); + } + else + { + currentMsg.content+=msg.content; + } + + }, setMsgList(value) { this.msgList = value; }, diff --git a/Yi.Bbs.Vue3/src/utils/icon.js b/Yi.Bbs.Vue3/src/utils/icon.js index 3a1fcaff..4d8bd29c 100644 --- a/Yi.Bbs.Vue3/src/utils/icon.js +++ b/Yi.Bbs.Vue3/src/utils/icon.js @@ -2,10 +2,14 @@ export const getUrl = (fileId) => { if (fileId == null || fileId == undefined) { return "/acquiesce.png" } - else { - return getEnvUrl(fileId) + if (fileId.startsWith(`${import.meta.env.VITE_APP_BASEAPI}`)) { + return fileId; + } + + return getEnvUrl(fileId) + }; const getEnvUrl = (str) => { diff --git a/Yi.Bbs.Vue3/src/views/chathub/Index.vue b/Yi.Bbs.Vue3/src/views/chathub/Index.vue index 71db7d70..14dc07ab 100644 --- a/Yi.Bbs.Vue3/src/views/chathub/Index.vue +++ b/Yi.Bbs.Vue3/src/views/chathub/Index.vue @@ -3,7 +3,7 @@ import { onMounted, ref, computed, onUnmounted } from 'vue'; import { storeToRefs } from 'pinia' import useAuths from '@/hooks/useAuths.js'; import { getList as getChatUserList } from '@/apis/chatUserApi' -import { sendPersonalMessage, sendGroupMessage, getAccountList as getChatAccountMessageList } from '@/apis/chatMessageApi' +import { sendPersonalMessage, sendGroupMessage, getAccountList as getChatAccountMessageList, sendAiChat } from '@/apis/chatMessageApi' import useChatStore from "@/stores/chat"; import useUserStore from "@/stores/user"; const { isLogin } = useAuths(); @@ -19,17 +19,22 @@ const userStore = useUserStore(); //发送消息是否为空 const msgIsNullShow = ref(false) //当前选择用户 -const currentSelectUser = ref(null); +const currentSelectUser = ref('all'); //当前输入框的值 const currentInputValue = ref(""); //临时存储的输入框,根据用户id及组name、all组为key,data为value -const inputListDataStore = ref([{ key: "all", value: "" }]); +const inputListDataStore = ref([{ key: "all", value: "" }, { key: "ai", value: "" }]); +//AI聊天临时存储 +const sendAiChatContext = ref([]); //当前聊天框显示的消息 const currentMsgContext = computed(() => { if (selectIsAll()) { return chatStore.allMsgContext; } + else if (selectIsAi) { + return chatStore.aiMsgContext; + } else { return chatStore.personalMsgContext.filter(x => { //两个条件 @@ -40,11 +45,31 @@ const currentMsgContext = computed(() => { }); } }); +const getChatUrl=(url,position)=> +{ + if(position=="left" && selectIsAi()) + { + return "/openAi.png" + } + return getUrl(url); + +} //当前聊天框显示的名称 const currentHeaderName = computed(() => { - return currentSelectUser.value == null ? "官方学习交流群" : currentSelectUser.value.userName; + if (selectIsAll()) { + return "官方学习交流群"; + + } + else if + (selectIsAi()) { + return "Ai-ChatGpt(你的私人ai小助手)" + } + else { + currentSelectUser.value.userName; + } + }); const currentUserItem = computed(() => { return userList.value.filter(x => x.userId != useUserStore().id) @@ -76,8 +101,13 @@ onUnmounted(() => { /*-----方法-----*/ //当前选择的是否为全部 const selectIsAll = () => { - return currentSelectUser.value == null; + return currentSelectUser.value == 'all'; }; +//当前选择的是否为Ai +const selectIsAi = () => { + return currentSelectUser.value == 'ai'; +}; + //输入框的值被更改 const changeInputValue = (inputValue) => { @@ -85,7 +115,10 @@ const changeInputValue = (inputValue) => { let index = -1; let findKey = currentSelectUser.value?.userId if (selectIsAll()) { - findKey = 'all' + findKey = 'all'; + } + else if (selectIsAi()) { + findKey = 'ai'; } index = inputListDataStore.value.findIndex(obj => obj.key == findKey); inputListDataStore.value[index].value = currentInputValue.value; @@ -99,7 +132,12 @@ const getCurrentInputValue = () => { if (selectIsAll()) { return inputListDataStore.value.filter(x => x.key == "all")[0].value; - } else { + } + else if (selectIsAi()) { + return inputListDataStore.value.filter(x => x.key == "ai")[0].value; + } + + else { //如果不存在初始存储值 if (!inputListDataStore.value.some(x => x.key == currentSelectUser.value.userId)) { inputListDataStore.value.push({ key: currentSelectUser.value.userId, value: "" }); @@ -110,9 +148,12 @@ const getCurrentInputValue = () => { }; //点击用户列表, -const onclickUserItem = (userInfo, isAllItem) => { - if (isAllItem) { - currentSelectUser.value = null; +const onclickUserItem = (userInfo, itemType) => { + if (itemType == "all") { + currentSelectUser.value = 'all'; + } + else if (itemType == "ai") { + currentSelectUser.value = 'ai'; } else { currentSelectUser.value = userInfo; @@ -136,6 +177,17 @@ const onclickSendMsg = () => { if (selectIsAll()) { onclickSendGroupMsg("all", currentInputValue.value); + } + else if (selectIsAi()) { + //ai消息需要将上下文存储 + sendAiChatContext.value.push({ answererType: 'User', message: currentInputValue.value, number: sendAiChatContext.value.length }) + + //离线前端存储 + chatStore.addMsg({messageType:"Ai",content:currentInputValue.value,sendUserId:userStore.id,sendUserInfo:{user:{icon:userStore.icon}}}) + //发送ai消息 + sendAiChat(sendAiChatContext.value); + + } else { onclickSendPersonalMsg(currentSelectUser.value.userId, currentInputValue.value); @@ -184,10 +236,15 @@ const onclickSendGroupMsg = (groupName, msg) => { //获取当前最后一条信息 -const getLastMessage = ((receiveId, isAll) => { - if (isAll) { - return chatStore.allMsgContext[chatStore.allMsgContext.length - 1]?.content; - } else { +const getLastMessage = ((receiveId, itemType) => { + if (itemType == "all") { + return chatStore.allMsgContext[chatStore.allMsgContext.length - 1]?.content.substring(0, 15); + } + else if (itemType == "ai") { + return chatStore.aiMsgContext[chatStore.aiMsgContext.length - 1]?.content.substring(0, 15); + } + + else { const messageContext = chatStore.personalMsgContext.filter(x => { //两个条件 //接收用户者id为对面id(我发给他) @@ -195,7 +252,7 @@ const getLastMessage = ((receiveId, isAll) => { return (x.receiveId == receiveId && x.sendUserId == userStore.id) || (x.sendUserId == receiveId && x.receiveId == userStore.id); }); - return messageContext[messageContext.length - 1]?.content; + return messageContext[messageContext.length - 1]?.content.substring(0, 15); } }) @@ -203,16 +260,17 @@ const getLastMessage = ((receiveId, isAll) => {