From 25929483c3dc8c8125299f456deb02b6ed3b297b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A9=99=E5=AD=90?= <454313500@qq.com> Date: Sun, 2 Feb 2025 00:22:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9Aai=E8=81=8A?= =?UTF-8?q?=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/AiChatService.cs | 9 +- .../Managers/AiManager.cs | 15 +- .../src/assets/chat_images/deepSeekAi.png | Bin 0 -> 25937 bytes .../assets/chat_images}/openAi.png | Bin Yi.Bbs.Vue3/src/views/chathub/Index.vue | 660 +++++++++--------- 5 files changed, 331 insertions(+), 353 deletions(-) create mode 100644 Yi.Bbs.Vue3/src/assets/chat_images/deepSeekAi.png rename Yi.Bbs.Vue3/{public => src/assets/chat_images}/openAi.png (100%) 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 8604ab46..f16d371e 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 @@ -23,14 +23,13 @@ namespace Yi.Framework.ChatHub.Application.Services /// /// [Authorize] - [HttpPost] - - public async Task ChatAsync([FromBody] List chatContext) + [HttpPost("ai-chat/chat/{model}")] + public async Task ChatAsync([FromRoute]string model, [FromBody] List chatContext) { const int maxChar = 10; var contextId = Guid.NewGuid(); Queue stringQueue = new Queue(); - await foreach (var aiResult in _aiManager.ChatAsStreamAsync(chatContext)) + await foreach (var aiResult in _aiManager.ChatAsStreamAsync(model,chatContext)) { stringQueue.Enqueue(aiResult); @@ -53,8 +52,6 @@ namespace Yi.Framework.ChatHub.Application.Services currentEndStr.Append(str); } await _userMessageManager.SendMessageAsync(MessageContext.CreateAi(currentEndStr.ToString(), 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/Managers/AiManager.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs index ddc81fd3..a8571f2b 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 @@ -24,19 +24,8 @@ namespace Yi.Framework.ChatHub.Domain.Managers } private OpenAIService OpenAIService { get; } - public async IAsyncEnumerable ChatAsStreamAsync(List aiChatContextDtos) + public async IAsyncEnumerable ChatAsStreamAsync(string model, List aiChatContextDtos) { - //var temp = "站长正在接入ChatGpt,敬请期待~"; - - //for (var i = 0; i < temp.Length; i++) - //{ - // await Task.Delay(200); - // yield return temp[i].ToString(); - //} - - - - if (aiChatContextDtos.Count == 0) { yield return null; @@ -56,7 +45,7 @@ namespace Yi.Framework.ChatHub.Domain.Managers var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest { Messages = messages, - Model = Models.Gpt_4o_mini + Model =model }); HttpStatusCode? error = null; diff --git a/Yi.Bbs.Vue3/src/assets/chat_images/deepSeekAi.png b/Yi.Bbs.Vue3/src/assets/chat_images/deepSeekAi.png new file mode 100644 index 0000000000000000000000000000000000000000..aa2fa5135a95c71d1f6229480c49313395026767 GIT binary patch literal 25937 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 -import { nextTick,onMounted, ref, computed, onUnmounted } from 'vue'; -import { storeToRefs } from 'pinia' +import {nextTick, 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, sendAiChat } from '@/apis/chatMessageApi' +import {getList as getChatUserList} from '@/apis/chatUserApi' +import { + sendPersonalMessage, + sendGroupMessage, + getAccountList as getChatAccountMessageList, + sendAiChat +} from '@/apis/chatMessageApi' import useChatStore from "@/stores/chat"; import useUserStore from "@/stores/user"; -const { isLogin } = useAuths(); -import { useRouter } from 'vue-router' -import { getUrl } from '@/utils/icon' + +const {isLogin} = useAuths(); +import {useRouter} from 'vue-router' +import {getUrl} from '@/utils/icon' //markdown ai显示 -import { marked } from 'marked'; +import {marked} from 'marked'; import '@/assets/atom-one-dark.css'; import '@/assets/github-markdown.css'; import hljs from "highlight.js"; -const isShowTipNumber=ref(10); +const isShowTipNumber = ref(10); const router = useRouter(); //聊天存储 const chatStore = useChatStore(); -const { userList } = storeToRefs(chatStore); +const {userList} = storeToRefs(chatStore); //用户信息 const userStore = useUserStore(); @@ -31,153 +37,18 @@ const currentSelectUser = ref('all'); //当前输入框的值 const currentInputValue = ref(""); //临时存储的输入框,根据用户id及组name、all组为key,data为value -const inputListDataStore = ref([{ key: "all", value: "" }, { key: "ai", value: "" }]); +const inputListDataStore = ref([ + {key: "all", name:"官方学习交流群",titleName:"官方学习交流群",logo:"yilogo.png", value: ""}, + {key: "ai@deepseek-chat",name:"DeepSeek聊天", titleName:"DeepSeek-聊天模式",logo:"deepSeekAi.png",value: ""}, + {key: "ai@deepseek-reasoner",name:"DeepSeek思索",titleName:"DeepSeek-思索模式",logo:"deepSeekAi.png", value: ""}, + {key: "ai@gpt-4o-mini",name:"ChatGpt聊天",titleName:"ChatGpt-聊天模式",logo:"openAi.png", value: ""}, +]); //AI聊天临时存储 const sendAiChatContext = ref([]); - -let timerTip=null; -//倒计时显示tip -const startCountTip = () => { - timerTip = setInterval(() => { - if (isShowTipNumber.value > 0) { - isShowTipNumber.value--; - } else { - clearInterval(timerTip); // 倒计时结束 - } - }, 1000); - }; -let codeCopyDic=[]; -//当前聊天框显示的消息 -const currentMsgContext = computed(() => { - - if (selectIsAll()) { - return chatStore.allMsgContext; - } - else if (selectIsAi()) { - //如果是ai的值,还行经过markdown处理 - // console.log(chatStore.aiMsgContext, "chatStore.aiMsgContext"); - // return chatStore.aiMsgContext; - let tempHtml = []; - codeCopyDic=[]; - chatStore.aiMsgContext.forEach(element => { - - tempHtml.push({ content: toMarkDownHtml(element.content), messageType: 'Ai', sendUserId: element.sendUserId, sendUserInfo: element.sendUserInfo }); - }); - - return tempHtml; - } - else { - return chatStore.personalMsgContext.filter(x => { - //两个条件 - //接收用户者id为对面id(我发给他) - //或者,发送用户id为对面(他发给我) - return (x.receiveId == currentSelectUser.value.userId && x.sendUserId == userStore.id) || - (x.sendUserId == currentSelectUser.value.userId && x.receiveId == userStore.id); - }); - } -}); - -//转换markdown -const toMarkDownHtml = (text) => { - marked.setOptions({ - renderer: new marked.Renderer(), - highlight: function (code, language) { - return codeHandler(code, language); - //return hljs.highlightAuto(code).value; - }, - pedantic: false, - gfm: true,//允许 Git Hub标准的markdown - tables: true,//支持表格 - breaks: true, - sanitize: false, - smartypants: false, - xhtml: false, - smartLists: true, - } - ); - //需要注意代码块样式 - const soureHtml = marked(text); - nextTick(()=>{ - addCopyEvent(); - }) - return soureHtml; -} - - -//code部分处理、高亮 -const codeHandler = (code, language) => { - const codeIndex = parseInt(Date.now() + "") + Math.floor(Math.random() * 10000000); - //console.log(codeIndex,"codeIndex"); - // 格式化第一行是右侧language和 “复制” 按钮; - if (code) { - const navCode = navHandler(code) - try { - // 使用 highlight.js 对代码进行高亮显示 - const preCode = hljs.highlightAuto(code).value; - // 将代码包裹在 textarea 中,由于防止textarea渲染出现问题,这里将 "<" 用 "<" 代替,不影响复制功能 - let html = `
${language}复制代码
${preCode}
`; - codeCopyDic.push({ id: codeIndex, code: code }); - // console.log(codeCopyDic.length); - return html; - // - } catch (error) { - console.log(error); - } - } - -} - -//左侧导航栏处理 -const navHandler = (code) => { - //获取行数 - var linesCount = getLinesCount(code); - - var currentLine = 1; - var liHtml = ``; - while (linesCount + 1 >= currentLine) { - liHtml += `` - currentLine++ - } - - let html = `` - - - return html; -} -const BREAK_LINE_REGEXP = /\r\n|\r|\n/g; -const getLinesCount = (text) => { - return (text.trim().match(BREAK_LINE_REGEXP) || []).length; -} - -const getChatUrl = (url, position) => { - if (position == "left" && selectIsAi()) { - return "/openAi.png" - } - - return getUrl(url); - -} - -//当前聊天框显示的名称 -const currentHeaderName = computed(() => { - if (selectIsAll()) { - return "官方学习交流群"; - } - else if - (selectIsAi()) { - return "Ai-ChatGpt4.0(你的私人ai小助手)" - } - else { - - return currentSelectUser.value.userName; - } - -}); -const currentUserItem = computed(() => { - return userList.value.filter(x => x.userId != useUserStore().id) -}); var timer = null; +let codeCopyDic = []; + //初始化 onMounted(async () => { if (!isLogin.value) { @@ -188,12 +59,10 @@ onMounted(async () => { timer = setTimeout(function () { onclickClose(); }, 3000); - - } chatStore.setMsgList((await getChatAccountMessageList()).data); chatStore.setUserList((await getChatUserList()).data); - startCountTip(); + startCountTip(); }) onUnmounted(() => { if (timer != null) { @@ -202,90 +71,113 @@ onUnmounted(() => { if (timerTip != null) { clearInterval(timerTip) } - }) -const clickCopyEvent=async function(event) { - const spanId=event.target.id; - console.log(codeCopyDic,"codeCopyDic") - console.log(spanId,"spanId") - await navigator.clipboard.writeText(codeCopyDic.filter(x=>x.id==spanId)[0].code); - ElMessage({ - message: "代码块复制成功", - type: "success", - duration: 2000, - }); -} -//代码copy事件 -const addCopyEvent=()=>{ - const copySpans = document.querySelectorAll('.copy'); -// 为每个 copy span 元素添加点击事件 -copySpans.forEach(span => { - //先移除,再新增 - span.removeEventListener('click',clickCopyEvent ); - span.addEventListener('click', clickCopyEvent); -}); -} +/*-----计算属性-----*/ +//当前聊天框内容显示的消息 +const currentMsgContext = computed(() => { + //选择全部人 + if (selectIsAll()) { + return chatStore.allMsgContext; + } + //选择ai + else if (selectIsAi()) { + //如果是ai的值,还行经过markdown处理 + // console.log(chatStore.aiMsgContext, "chatStore.aiMsgContext"); + // return chatStore.aiMsgContext; + let tempHtml = []; + codeCopyDic = []; + chatStore.aiMsgContext.forEach(element => { + tempHtml.push({ + content: toMarkDownHtml(element.content), + messageType: 'Ai', + sendUserId: element.sendUserId, + sendUserInfo: element.sendUserInfo + }); + }); + return tempHtml; + } + //选择个人 + else { + return chatStore.personalMsgContext.filter(x => { + //两个条件 + //接收用户者id为对面id(我发给他) + //或者,发送用户id为对面(他发给我) + return (x.receiveId === currentSelectUser.value.userId && x.sendUserId === userStore.id) || + (x.sendUserId === currentSelectUser.value.userId && x.receiveId === userStore.id); + }); + } +}); +//图片 +const getChatUrl = (url, position) => { + if (position === "left" && selectIsAi()) { + return "/openAi.png" + } + return getUrl(url); +} +//当前聊天框显示的名称 +const currentHeaderName = computed(() => { + if (selectIsAll()) { + return "官方学习交流群"; + } else if (selectIsAi()) { + return "Ai-ChatGpt4.0(你的私人ai小助手)" + } else { + return currentSelectUser.value.userName; + } +}); +//当前在线的用户项 +const currentUserItem = computed(() => { + return userList.value.filter(x => x.userId !== useUserStore().id) +}); /*-----方法-----*/ //当前选择的是否为全部 const selectIsAll = () => { - return currentSelectUser.value == 'all'; + return currentSelectUser.value === 'all'; }; //当前选择的是否为Ai const selectIsAi = () => { - return currentSelectUser.value == 'ai'; + return currentSelectUser.value === 'ai'; }; - -//输入框的值被更改 +//输入框的值被更改(同时保存到store中) const changeInputValue = (inputValue) => { currentInputValue.value = inputValue; let index = -1; let findKey = currentSelectUser.value?.userId if (selectIsAll()) { findKey = 'all'; - } - else if (selectIsAi()) { + } else if (selectIsAi()) { findKey = 'ai'; } - index = inputListDataStore.value.findIndex(obj => obj.key == findKey); + index = inputListDataStore.value.findIndex(obj => obj.key === findKey); inputListDataStore.value[index].value = currentInputValue.value; } -//绑定的input改变事件 -const updateInputValue = (event) => { - changeInputValue(event.target.value); -} -//获取输入框的值 + +//获取输入框的值(切换左侧菜单的时候,给输入框补充之前没发送的值) const getCurrentInputValue = () => { - if (selectIsAll()) { - return inputListDataStore.value.filter(x => x.key == "all")[0].value; - } - else if (selectIsAi()) { - return inputListDataStore.value.filter(x => x.key == "ai")[0].value; - } - - else { + return inputListDataStore.value.filter(x => x.key === "all")[0].value; + } 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: "" }); + if (!inputListDataStore.value.some(x => x.key === currentSelectUser.value.userId)) { + inputListDataStore.value.push({key: currentSelectUser.value.userId, value: ""}); return ""; } - return inputListDataStore.value.filter(x => x.key == currentSelectUser.value.userId)[0].value; + return inputListDataStore.value.filter(x => x.key === currentSelectUser.value.userId)[0].value; } }; //点击用户列表, const onclickUserItem = (userInfo, itemType) => { - if (itemType == "all") { + if (itemType === "all") { currentSelectUser.value = 'all'; - } - else if (itemType == "ai") { + } else if (itemType === "ai") { currentSelectUser.value = 'ai'; - } - else { + } else { currentSelectUser.value = userInfo; } //填充临时存储的输入框 @@ -294,26 +186,9 @@ const onclickUserItem = (userInfo, itemType) => { changeInputValue(value); } -//输入框按键事件 -const handleKeydownInput=()=>{ - // 检查是否按下 Shift + Enter - if (event.key === 'Enter' && event.shiftKey) { - // 允许输入换行 - return; // 让默认行为继续 - } - - // 如果只按下 Enter,则阻止默认的提交行为,比如在表单中 - if (event.key === 'Enter') { - // 阻止默认行为 - event.preventDefault(); - onclickSendMsg(); - return; - } -} - //点击发送按钮 const onclickSendMsg = () => { - if (currentInputValue.value == "") { + if (currentInputValue.value === "") { msgIsNullShow.value = true; setTimeout(() => { // 这里写上你想要3秒后执行的代码 @@ -324,19 +199,24 @@ const onclickSendMsg = () => { if (selectIsAll()) { onclickSendGroupMsg("all", currentInputValue.value); - } - else if (selectIsAi()) { + } else if (selectIsAi()) { //ai消息需要将上下文存储 - sendAiChatContext.value.push({ answererType: 'User', message: currentInputValue.value, number: sendAiChatContext.value.length }) + 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 } } }) + chatStore.addMsg({ + messageType: "Ai", + content: currentInputValue.value, + sendUserId: userStore.id, + sendUserInfo: {user: {icon: userStore.icon}} + }) //发送ai消息 sendAiChat(sendAiChatContext.value); - - - } - else { + } else { onclickSendPersonalMsg(currentSelectUser.value.userId, currentInputValue.value); } changeInputValue(""); @@ -351,80 +231,197 @@ const onclickSendPersonalMsg = (receiveId, msg) => { content: msg, receiveId: receiveId }); - sendPersonalMessage({ userId: receiveId, content: msg }); + sendPersonalMessage({userId: receiveId, content: msg}); //调用接口发送消息 } -const onclickClose = () => { - router.push({ path: "/index" }) - .then(() => { - // 重新刷新页面 - location.reload() - }) -} - //点击发送群组消息按钮 const onclickSendGroupMsg = (groupName, msg) => { //组还需区分是否给全部成员组 if (selectIsAll) { - //添加到本地存储,不需要,因为广播自己能够接收 - // chatStore.addMsg({ - // messageType: "All", - // sendUserId: userStore.id, - // content: msg - // }); - //调用接口发送消息 - sendGroupMessage({ content: msg }); - } - else { + sendGroupMessage({content: msg}); + } else { alert("暂未实现"); } } -//清除ai对话 -const clearAiMsg = () => { - sendAiChatContext.value = []; - chatStore.clearAiMsg(); - ElMessage({ - message: "当前会话清除成功", - type: "success", - duration: 2000, - }); - -} -//获取当前最后一条信息 +//获取当前最后一条信息(显示在左侧菜单) const getLastMessage = ((receiveId, itemType) => { - if (itemType == "all") { + if (itemType === "all") { return chatStore.allMsgContext[chatStore.allMsgContext.length - 1]?.content.substring(0, 15); - } - else if (itemType == "ai") { + } else if (itemType === "ai") { return chatStore.aiMsgContext[chatStore.aiMsgContext.length - 1]?.content.substring(0, 15); - } - - else { + } else { const messageContext = chatStore.personalMsgContext.filter(x => { //两个条件 //接收用户者id为对面id(我发给他) //或者,发送用户id为对面(他发给我) - return (x.receiveId == receiveId && x.sendUserId == userStore.id) || - (x.sendUserId == receiveId && x.receiveId == userStore.id); + return (x.receiveId === receiveId && x.sendUserId === userStore.id) || + (x.sendUserId === receiveId && x.receiveId === userStore.id); }); return messageContext[messageContext.length - 1]?.content.substring(0, 15); } }) + + +/*----------以下方法是对内容通用处理,没有业务逻辑,无需变化----------*/ + const imageSrc=(imageName)=> { + // 动态拼接路径 + return new URL(`../../assets/chat_images/${imageName}`, import.meta.url).href; + } + + //绑定的input改变事件 + const updateInputValue = (event) => { + changeInputValue(event.target.value); + } + + //输入框按键事件 + const handleKeydownInput = () => { + // 检查是否按下 Shift + Enter + if (event.key === 'Enter' && event.shiftKey) { + // 允许输入换行 + return; // 让默认行为继续 + } + // 如果只按下 Enter,则阻止默认的提交行为,比如在表单中 + if (event.key === 'Enter') { + // 阻止默认行为 + event.preventDefault(); + onclickSendMsg(); + return; + } + } + + //关闭聊天框 + const onclickClose = () => { + router.push({path: "/index"}) + .then(() => { + // 重新刷新页面 + location.reload() + }) + } + +//清除ai对话 + const clearAiMsg = () => { + sendAiChatContext.value = []; + chatStore.clearAiMsg(); + ElMessage({ + message: "当前会话清除成功", + type: "success", + duration: 2000, + }); + + } + + +//转换markdown + const toMarkDownHtml = (text) => { + marked.setOptions({ + renderer: new marked.Renderer(), + highlight: function (code, language) { + return codeHandler(code, language); + }, + pedantic: false, + gfm: true,//允许 Git Hub标准的markdown + tables: true,//支持表格 + breaks: true, + sanitize: false, + smartypants: false, + xhtml: false, + smartLists: true, + } + ); + //需要注意代码块样式 + const soureHtml = marked(text); + nextTick(() => { + addCopyEvent(); + }) + return soureHtml; + } +//code部分处理、高亮 + const codeHandler = (code, language) => { + const codeIndex = parseInt(Date.now() + "") + Math.floor(Math.random() * 10000000); + //console.log(codeIndex,"codeIndex"); + // 格式化第一行是右侧language和 “复制” 按钮; + if (code) { + const navCode = navHandler(code) + try { + // 使用 highlight.js 对代码进行高亮显示 + const preCode = hljs.highlightAuto(code).value; + // 将代码包裹在 textarea 中,由于防止textarea渲染出现问题,这里将 "<" 用 "<" 代替,不影响复制功能 + let html = `
${language}复制代码
${preCode}
`; + codeCopyDic.push({id: codeIndex, code: code}); + // console.log(codeCopyDic.length); + return html; + // + } catch (error) { + console.log(error); + } + } + + } +//左侧导航栏处理 + const navHandler = (code) => { + //获取行数 + var linesCount = getLinesCount(code); + var currentLine = 1; + var liHtml = ``; + while (linesCount + 1 >= currentLine) { + liHtml += `` + currentLine++ + } + + let html = `` + return html; + } + const BREAK_LINE_REGEXP = /\r\n|\r|\n/g; + const getLinesCount = (text) => { + return (text.trim().match(BREAK_LINE_REGEXP) || []).length; + } + + let timerTip = null; +//倒计时显示tip + const startCountTip = () => { + timerTip = setInterval(() => { + if (isShowTipNumber.value > 0) { + isShowTipNumber.value--; + } else { + clearInterval(timerTip); // 倒计时结束 + } + }, 1000); + }; +//代码copy事件 + const addCopyEvent = () => { + const copySpans = document.querySelectorAll('.copy'); +// 为每个 copy span 元素添加点击事件 + copySpans.forEach(span => { + //先移除,再新增 + span.removeEventListener('click', clickCopyEvent); + span.addEventListener('click', clickCopyEvent); + }); + } + const clickCopyEvent = async function (event) { + const spanId = event.target.id; + console.log(codeCopyDic, "codeCopyDic") + console.log(spanId, "spanId") + await navigator.clipboard.writeText(codeCopyDic.filter(x => x.id === spanId)[0].code); + ElMessage({ + message: "代码块复制成功", + type: "success", + duration: 2000, + }); + }