diff --git a/.env b/.env index d13f88a..1f625b3 100644 --- a/.env +++ b/.env @@ -5,4 +5,10 @@ VITE_GLOB_APP_TITLE = 青鸟语言大模型-同聪 VITE_GLOB_APP_SHORT_NAME = 同聪 # token key -VITE_GLOB_APP_TOKEN_KEY = "hulk-Auth" \ No newline at end of file +VITE_GLOB_APP_TOKEN_KEY = "hulk-Auth" + +# authenticationScheme +VITE_GLOB_APP_AUTHENTICATION_SCHEME = 'Basic' + +# tokenScheme +VITE_GLOB_APP_TOKEN_SCHEME = 'crypto' \ No newline at end of file diff --git a/src/api/base/message.ts b/src/api/base/message.ts index f15a8ed..8f13928 100644 --- a/src/api/base/message.ts +++ b/src/api/base/message.ts @@ -1,9 +1,11 @@ import { defHttp } from '@/utils/axios/index' +import type { MenuTypeEnum } from '@/enums/menuEnum' /** * @description 新建会话 */ export async function addMessage(data: { + type: MenuTypeEnum title: string }) { return defHttp.post({ @@ -34,9 +36,9 @@ export async function removeMessage(ids: string) { /** * @description 对话列表 */ -export async function conversationList() { +export async function conversationList(type: number) { return defHttp.get({ - url: `/open-chat/chat/conversation/list`, + url: `/open-chat/chat/conversation/list?type=${type}`, }) } @@ -55,10 +57,9 @@ export async function historyMessage(params: { } /** - * @description 发送消息 + * @description 发送消息(文生文) */ -export async function sendMessage(data: { - roleId: number +export async function sendTextToText(data: { conversationId: string question: string }) { @@ -69,6 +70,20 @@ export async function sendMessage(data: { }) } +/** + * @description 发送消息(文生图) + */ +export async function sendTextToImage(data: { + conversationId: string + question: string +}) { + return defHttp.post({ + url: `/open-gpts/gpts/getDallEImages`, + data, + timeout: 30 * 1000, + }) +} + /** * @description 获取chat信息 */ diff --git a/src/components/AppMessage/index.vue b/src/components/AppMessage/index.vue index 66a77e0..c955279 100644 --- a/src/components/AppMessage/index.vue +++ b/src/components/AppMessage/index.vue @@ -1,11 +1,14 @@ + + + + diff --git a/src/enums/menuEnum.ts b/src/enums/menuEnum.ts index 0daccda..d1275fe 100644 --- a/src/enums/menuEnum.ts +++ b/src/enums/menuEnum.ts @@ -1,22 +1,28 @@ export enum MenuTypeEnum { - // 会话 - CONVERSATION = 'conversation', + // 文生文 + TEXT_TO_TEXT = 1, - // 文生图 - TEXT_TO_PICTURE = 'textToPicture', + // 文生图 + TEXT_TO_IMAGE, - // 角色 - ROLE = 'role', + // 角色 + ROLE, - // 任务 + // 视觉分析 + VISUAL_ANALYSIS, + + // 知识库 + REPOSITORY, + + // 任务 TASK = 'task', - // 渠道 + // 渠道 CHANNEL = 'channel', - // 小程序 + // 小程序 APPLET = 'applet', - // 我的 + // 我的 USER = 'user', } diff --git a/src/hooks/useMqtt.ts b/src/hooks/useMqtt.ts index 576acf1..bcc7e4f 100644 --- a/src/hooks/useMqtt.ts +++ b/src/hooks/useMqtt.ts @@ -1,14 +1,17 @@ +import { ref } from 'vue' import type { Options } from '@/utils/mqtt' import { MqttService } from '@/utils/mqtt' import { useUserStore } from '@/store/moules/userStore/index' import { useMessageStore } from '@/store/moules/messageStore/index' import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' -import { sendMessage as sendMessageApi } from '@/api/base/message' +import { MenuTypeEnum } from '@/enums/menuEnum' export function useMqtt() { const userStore = useUserStore() const messageStore = useMessageStore() + const messageData = ref('') + const options: Options = { host: import.meta.env.VITE_GLOB_MQTT_HOST, port: import.meta.env.VITE_GLOB_MQTT_PORT, @@ -30,22 +33,22 @@ export function useMqtt() { mqttService .subscribe(topicKey) .then(() => { - mqttService.onMessage(topicKey, (messageData: any) => { - if (messageData.message_type === MessageStatusEnum.ACTICON) { + mqttService.onMessage(topicKey, (message: any) => { + if (message.message_type === MessageStatusEnum.ACTICON) { messageStore.setMessageStatus(MessageStatusEnum.ACTICON) messageStore.setMessageLastItem({ messageType: MessageTypeEnum.AI, - content: messageData.message_content, + content: message.message_content, time: String(new Date().getTime()), avatar: '', messageStatus: MessageStatusEnum.ACTICON, }) } - if (messageData.message_type === MessageStatusEnum.END) { + if (message.message_type === MessageStatusEnum.END) { messageStore.setMessageStatus(MessageStatusEnum.END) messageStore.setMessageLastItem({ messageType: MessageTypeEnum.AI, - content: messageData.message_content, + content: message.message_content, time: String(new Date().getTime()), avatar: '', messageStatus: MessageStatusEnum.END, @@ -91,45 +94,11 @@ export function useMqtt() { mqttService.end() } - /** - * @description: 发送消息hook - */ - const sendMessage = async (roleId: number, conversationId: string, question: string): Promise => { - messageStore.setMessageStatus(MessageStatusEnum.LOADING) - messageStore.setMessagePushItem({ - messageType: MessageTypeEnum.USER, - content: question, - time: String(new Date().getTime()), - avatar: '', - }) - messageStore.setMessagePushItem({ - messageType: MessageTypeEnum.AI, - content: '正在思考中...', - time: String(new Date().getTime()), - avatar: '', - messageStatus: MessageStatusEnum.LOADING, - }) - if (!messageStore.getConversationData) { - return - } - - try { - await sendMessageApi({ - roleId, - conversationId, - question, - }) - } - catch (error: any) { - messageStore.setMessageStatus(MessageStatusEnum.END) - } - } - return { + messageData, connect, subscribe, unsubscribe, end, - sendMessage, } } diff --git a/src/layout/AppMenu/index.vue b/src/layout/AppMenu/index.vue index 979343d..59b8c34 100644 --- a/src/layout/AppMenu/index.vue +++ b/src/layout/AppMenu/index.vue @@ -6,19 +6,19 @@ import { SvgIcon } from '@/components/SvgIcon' import { MenuTypeEnum } from '@/enums/menuEnum' const router = useRouter() -const menuActive = ref(MenuTypeEnum.CONVERSATION) +const menuActive = ref(MenuTypeEnum.TEXT_TO_TEXT) const menu = ref([ { name: '会话', icon: 'wei_xin', path: '/conversation', - key: MenuTypeEnum.CONVERSATION, + key: MenuTypeEnum.TEXT_TO_TEXT, }, { name: '文生图', icon: 'wen_sheng_tu', - path: '/textToPicture', - key: MenuTypeEnum.TEXT_TO_PICTURE, + path: '/textToImage', + key: MenuTypeEnum.TEXT_TO_IMAGE, }, { name: '角色', diff --git a/src/router/index.ts b/src/router/index.ts index 13eb749..ff3fa56 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -41,8 +41,8 @@ export const constantRoutes: Array = [ }, { name: 'TextToPicture', - path: '/textToPicture', - component: () => import('@/views/textToPicture/index.vue'), + path: '/textToImage', + component: () => import('@/views/textToImage/index.vue'), meta: { title: '文生图', }, diff --git a/src/store/moules/messageStore/index.d.ts b/src/store/moules/messageStore/index.d.ts index 2c1ae01..01f8188 100644 --- a/src/store/moules/messageStore/index.d.ts +++ b/src/store/moules/messageStore/index.d.ts @@ -9,6 +9,7 @@ export interface ConversationData { id: string isDeleted: number roleId: number + type: number status: number title: string updateTime: string diff --git a/src/utils/axios/index.ts b/src/utils/axios/index.ts index 7062147..0d11e5f 100644 --- a/src/utils/axios/index.ts +++ b/src/utils/axios/index.ts @@ -2,7 +2,7 @@ // The axios configuration can be changed according to the project, just change the file, other files can be left unchanged import type { AxiosInstance, AxiosResponse } from 'axios' -import { clone } from 'lodash-es' +import { clone, cloneDeep } from 'lodash-es' import axios from 'axios' import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform' import { VAxios } from './Axios' @@ -22,7 +22,7 @@ const { createMessage, createErrorModal, createSuccessModal } = useMessage() // 请求白名单,无须token的接口 const whiteList: string[] = ['/login', '/refresh-token'] // 不需要解密接口白名单 -const notDecryptWhiteList = ['/hulk-auth/oauth/token'] +const notDecryptWhiteList = ['/hulk-auth/oauth/token', '/open-chat/chat/session', '/open-gpts/gpts/getDallEImages'] /** * @description: 数据处理,方便区分多种处理方式 @@ -33,9 +33,23 @@ const transform: AxiosTransform = { */ transformResponseHook: (res: AxiosResponse, options: RequestOptions) => { const { isTransformResponse, isReturnNativeResponse } = options + const newRes = cloneDeep(res) + let newData: Result + + if (strHasArr(res.config.url!, notDecryptWhiteList)) { + newData = newRes.data + } + + if (import.meta.env.VITE_GLOB_APP_TOKEN_SCHEME) { + newData = JSON.parse(crypto.decryptAES(newRes.data as unknown as string, crypto.aesKey)) + } + else { + newData = newRes.data + } + // 二进制数据则直接返回 if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') - return res.data + return newData // 是否返回原生响应头 比如:需要获取响应头时使用该属性 if (isReturnNativeResponse) @@ -44,17 +58,11 @@ const transform: AxiosTransform = { // 不进行任何处理,直接返回 // 用于页面代码可能需要直接获取code,data,message这些信息时开启 if (!isTransformResponse) { - if (strHasArr(res.config.url!, notDecryptWhiteList)) { - return res.data - } - else { - return JSON.parse(crypto.decryptAES(res.data as unknown as string, crypto.aesKey)) - } + return newData } // 错误的时候返回 - const data = JSON.parse(crypto.decryptAES(res.data as unknown as string, crypto.aesKey)) - + const data = newData if (!data) { // return '[HTTP] Request has no return value'; throw new Error(HttpErrorMsgEnum.API_REQUEST_FAILED) @@ -267,8 +275,8 @@ function createAxios(opt?: Partial) { // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes // authentication schemes,e.g: Bearer - tokenScheme: 'crypto', - authenticationScheme: 'Basic', + tokenScheme: import.meta.env.VITE_GLOB_APP_TOKEN_SCHEME, + authenticationScheme: import.meta.env.VITE_GLOB_APP_AUTHENTICATION_SCHEME, timeout: 10 * 1000, // 基础接口地址 // baseURL: globSetting.apiUrl, diff --git a/src/views/conversation/index.vue b/src/views/conversation/index.vue index 022ed7e..b1f297f 100644 --- a/src/views/conversation/index.vue +++ b/src/views/conversation/index.vue @@ -14,8 +14,9 @@ import { AppTextarea } from '@/components/AppTextarea' import { AppMessage } from '@/components/AppMessage' import type { MessageItem } from '@/components/AppMessage/index.d' import { useMessageStore } from '@/store/moules/messageStore/index' +import { MenuTypeEnum } from '@/enums/menuEnum' import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' -import { addMessage, conversationList, historyMessage, removeMessage, updateMessage } from '@/api/base/message' +import { addMessage, conversationList, historyMessage, removeMessage, sendTextToText, updateMessage } from '@/api/base/message' import { useMqtt } from '@/hooks/useMqtt' import { useMessage } from '@/hooks/useMessage' @@ -28,7 +29,7 @@ const subMenuActiveIndex = ref(0) // 当前会话索引 const subMenuActionIndex = ref(-1) // 会话操作索引 const subMenuList = ref([]) const subMenuInputValue = ref('') -const appMessage = ref() +const appMessageRef = ref() const messageList = computed(() => messageStore.getMessageList) const messageStatus = computed(() => messageStore.getMessageStatus) const conversationData = computed(() => messageStore.getConversationData) @@ -116,17 +117,17 @@ async function handleSend(value: string) { if (subMenuActiveIndex.value === -1) { try { - await addMessage({ title: '新的对话' }) + await addMessage({ type: MenuTypeEnum.TEXT_TO_TEXT, title: '新的对话' }) await getConversationList() await nextTick() - useMqtt().sendMessage(conversationData.value.roleId, conversationData.value.id, value) + sendMessage(conversationData.value.id, value) } catch (error) { message.error('新建对话失败,请稍后重试!') } } else { - useMqtt().sendMessage(conversationData.value.roleId, conversationData.value.id, value) + sendMessage(conversationData.value.id, value) } } @@ -134,7 +135,7 @@ async function handleSend(value: string) { * @description: 获取会话列表 */ async function getConversationList() { - const res = await conversationList() + const res = await conversationList(MenuTypeEnum.TEXT_TO_TEXT) res.forEach((item: SubMenuItem) => { item.actionType = SubMenuActionEnum.NOT }) @@ -159,6 +160,8 @@ async function getHistoryMessage() { if (!res || !res.records || !res.records.length) { return } + console.log(res) + res.records.forEach((item: any) => { const itemData: MessageItem = { messageType: item.roleType === MessageTypeEnum.USER ? MessageTypeEnum.USER : MessageTypeEnum.AI, @@ -191,8 +194,40 @@ async function onScrollTop(scrollTop: number) { await getHistoryMessage() // 无缝滚动 - appMessage.value.seamlessScrollToTop() + appMessageRef.value.seamlessScrollToTop() + } +} + +/** + * @description: 发送消息hook + */ +async function sendMessage(conversationId: string, question: string): Promise { + conversationDefaultShow.value = false + messageStore.setMessageStatus(MessageStatusEnum.LOADING) + messageStore.setMessagePushItem({ + messageType: MessageTypeEnum.USER, + content: question, + time: String(new Date().getTime()), + avatar: '', + }) + messageStore.setMessagePushItem({ + messageType: MessageTypeEnum.AI, + content: '正在思考中...', + time: String(new Date().getTime()), + avatar: '', + messageStatus: MessageStatusEnum.LOADING, + }) + if (!messageStore.getConversationData) { + return } + + sendTextToText({ + conversationId, + question, + }).catch(() => { + messageStore.getMessageList.splice(-2) + messageStore.setMessageStatus(MessageStatusEnum.END) + }) } /** @@ -203,7 +238,7 @@ function reloadMessage() { return } const question = messageList.value[messageList.value.length - 2]?.content - useMqtt().sendMessage(conversationData.value.roleId, conversationData.value.id, question) + sendMessage(conversationData.value.id, question) } /** @@ -287,7 +322,7 @@ onBeforeRouteLeave(() => { +import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' +import { onBeforeRouteLeave } from 'vue-router' +import { Button, Spin, message } from 'ant-design-vue' +import { AppContainerBox } from '@/components/AppContainerBox' +import { AppSubMenuTitle } from '@/components/AppSubMenuTitle' +import { AppSubMenuList } from '@/components/AppSubMenuList' +import { AppTextToPictureDefault } from '@/components/AppTextToPictureDefault' +import { AppUserInfo } from '@/components/AppUserInfo' +import { SubMenuActionEnum } from '@/components/AppSubMenuList/index.d' +import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' + +import { AppTextarea } from '@/components/AppTextarea' +import { AppMessage } from '@/components/AppMessage' +import type { MessageItem } from '@/components/AppMessage/index.d' +import { useMessageStore } from '@/store/moules/messageStore/index' +import { MenuTypeEnum } from '@/enums/menuEnum' +import { MessageStatusEnum, MessageTypeEnum } from '@/enums/messageEnum' +import { addMessage, conversationList, historyMessage, removeMessage, sendTextToImage, updateMessage } from '@/api/base/message' +import { useMqtt } from '@/hooks/useMqtt' +import { useMessage } from '@/hooks/useMessage' + +const { createConfirm } = useMessage() + +const messageStore = useMessageStore() + +const sendBtnLoading = ref(false) +const subMenuActiveIndex = ref(0) // 当前会话索引 +const subMenuActionIndex = ref(-1) // 会话操作索引 +const subMenuList = ref([]) +const subMenuInputValue = ref('') +const appMessageRef = ref() +const messageList = computed(() => messageStore.getMessageList) +const messageStatus = computed(() => messageStore.getMessageStatus) +const conversationData = computed(() => messageStore.getConversationData) +const historyMessageParams = ref({ + conversationId: '', + current: 1, + size: 10, + total: 0, +}) + +const conversationDefaultShow = ref(false) +const appMessageShow = ref(true) +const spinning = ref(true) + +watch( + () => messageStatus.value, + (val) => { + if (val === MessageStatusEnum.END) { + sendBtnLoading.value = false + } + }, +) + +/** + * @description: 点击会话操作项 + */ +function handlesubMenuActionIndex(type: SubMenuActionEnum, item: SubMenuItem, index: number) { + subMenuActionIndex.value = index + if (type === SubMenuActionEnum.EDIT) { + subMenuList.value.forEach((item) => { + item.actionType = SubMenuActionEnum.NOT + }) + subMenuList.value[index].actionType = SubMenuActionEnum.EDIT + subMenuInputValue.value = item.title + } + + if (type === SubMenuActionEnum.DELETE) { + createConfirm({ + title: '删除', + content: `确定要删除${item.title}会话吗?`, + iconType: 'warning', + onOk: () => { + removeMessage(item.id).then(() => { + message.success('删除成功') + getConversationList() + }) + }, + }) + } +} + +/** + * @description: 切换会话 + */ +async function handleSubMenuChange(index: number) { + if (messageStatus.value !== MessageStatusEnum.END) { + message.warn('请先结束对话') + return + } + subMenuActiveIndex.value = index + historyMessageParams.value.current = 1 + historyMessageParams.value.total = 0 + messageStore.setConversationData(subMenuList.value[subMenuActiveIndex.value]) + messageStore.setMessageClear() + useMqtt().end() + useMqtt().connect() + getHistoryMessage() + + // 因为会话切换时滚动底部有问题,所以重新加载下 + appMessageShow.value = false + setTimeout(() => { + appMessageShow.value = true + }, 0) +} + +/** + * @description: 发送消息 + */ +async function handleSend(value: string) { + sendBtnLoading.value = true + conversationDefaultShow.value = false + if (!conversationData.value) { + return + } + + if (subMenuActiveIndex.value === -1) { + try { + await addMessage({ type: MenuTypeEnum.TEXT_TO_IMAGE, title: '新的对话' }) + await getConversationList() + await nextTick() + sendMessage(conversationData.value.id, value) + } + catch (error) { + message.error('新建对话失败,请稍后重试!') + } + } + else { + sendMessage(conversationData.value.id, value) + } +} + +/** + * @description: 获取会话列表 + */ +async function getConversationList() { + const res = await conversationList(MenuTypeEnum.TEXT_TO_IMAGE) + res.forEach((item: SubMenuItem) => { + item.actionType = SubMenuActionEnum.NOT + }) + console.log(res) + + subMenuList.value = res + subMenuActiveIndex.value = 0 + if (subMenuList.value.length) { + await handleSubMenuChange(0) + } +} + +/** + * @description: 获取历史对话记录 + */ +async function getHistoryMessage() { + if (!conversationData.value) { + return + } + spinning.value = true + historyMessageParams.value.conversationId = conversationData.value.id + const res = await historyMessage(historyMessageParams.value) + console.log('文生图', res) + + spinning.value = false + if (!res || !res.records || !res.records.length) { + return + } + res.records.forEach((item: any) => { + const itemData: MessageItem = { + messageType: item.roleType === MessageTypeEnum.USER ? MessageTypeEnum.USER : MessageTypeEnum.AI, + content: item.messageContent, + time: item.messageTime, + avatar: '', + messageStatus: MessageStatusEnum.END, + } + historyMessageParams.value.total = res.total + messageStore.setMessageUnshiftItem(itemData) + }) + + conversationDefaultShow.value = false +} + +/** + * @description: 滚动监听 + */ +async function onScrollTop(scrollTop: number) { + if (scrollTop !== 0) { + return + } + + if (historyMessageParams.value.current * historyMessageParams.value.size > historyMessageParams.value.total) { + return + } + + if (historyMessageParams.value.current < historyMessageParams.value.total) { + historyMessageParams.value.current++ + await getHistoryMessage() + + // 无缝滚动 + appMessageRef.value.seamlessScrollToTop() + } +} + +/** + * @description: 发送消息hook + */ +async function sendMessage(conversationId: string, question: string): Promise { + messageStore.setMessageStatus(MessageStatusEnum.LOADING) + messageStore.setMessagePushItem({ + messageType: MessageTypeEnum.USER, + content: question, + time: String(new Date().getTime()), + avatar: '', + }) + messageStore.setMessagePushItem({ + messageType: MessageTypeEnum.AI, + content: '正在思考中...', + time: String(new Date().getTime()), + avatar: '', + messageStatus: MessageStatusEnum.LOADING, + }) + if (!messageStore.getConversationData) { + return + } + + sendTextToImage({ + conversationId, + question, + }).catch(() => { + messageStore.getMessageList.splice(-2) + messageStore.setMessageStatus(MessageStatusEnum.END) + }) +} + +/** + * @description: 重新回答 + */ +function reloadMessage() { + if (!conversationData.value) { + return + } + const question = messageList.value[messageList.value.length - 2]?.content + sendMessage(conversationData.value.id, question) +} + +/** + * @description: 点击新建会话按钮 + */ +function handleAddMessage() { + conversationDefaultShow.value = true + subMenuActiveIndex.value = -1 +} + +// 下面是子菜单操作项 +function handleSubMenuInputAffirm(index: number, item: SubMenuItem, inputValue: string) { + updateMessage({ ...item, title: inputValue }).then(() => { + message.success('修改成功') + subMenuList.value[index].title = inputValue + subMenuList.value[index].actionType = SubMenuActionEnum.NOT + }) +} +function handleSubMenuInputClose(index: number) { + subMenuList.value[index].actionType = SubMenuActionEnum.NOT +} +function handleSubMenuInputBlur(index: number, item: SubMenuItem, inputValue: string) { + subMenuActionIndex.value = -1 + handleSubMenuInputAffirm(index, item, inputValue) +} + +onMounted(() => { + getConversationList() +}) + +onUnmounted(() => { + useMqtt().end() +}) + +// 路由离开时的操作 +onBeforeRouteLeave(() => { + if (messageStatus.value !== MessageStatusEnum.END) { + message.warn('请先结束对话') + return false + } +}) + + + + + diff --git a/src/views/textToPicture/index.vue b/src/views/textToPicture/index.vue deleted file mode 100644 index b7b4bd8..0000000 --- a/src/views/textToPicture/index.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/types/vite-env.d.ts b/types/vite-env.d.ts index a21e20f..68de614 100644 --- a/types/vite-env.d.ts +++ b/types/vite-env.d.ts @@ -11,6 +11,8 @@ interface ImportMetaEnv { VITE_GLOB_MQTT_PORT: number VITE_GLOB_MQTT_PROTOCOL: mqtt.MqttProtocol VITE_GLOB_APP_AUTHORIZATION: string + VITE_GLOB_APP_AUTHENTICATION_SCHEME: string + VITE_GLOB_APP_TOKEN_SCHEME: string VITE_GLOB_APP_TOKEN_KEY: string VITE_GLOB_MQTT_USERNAME: string VITE_GLOB_MQTT_PASSWORD: string