2025-04-10 | wujingjing | ![]() |
2025-04-09 | wujingjing | ![]() |
2025-04-09 | wujingjing | ![]() |
2025-04-09 | wujingjing | ![]() |
2025-04-09 | wujingjing | ![]() |
src/components/chat/components/ChatContainer.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/components/chat/components/playBar/hook/useDigitalHuman.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/component/header/Header.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/stores/chatRoom.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/components/chat/components/ChatContainer.vue
@@ -22,7 +22,7 @@ <i class="ywifont ywicon-xiangxiajiantou !text-[20px]" /> </div> </div> <div v-show="digitalHumanIsShow" v-loading="humanIsLoading" class="absolute right-28 bottom-[250px] w-[400px] h-[540px]"> <div v-show="digitalHumanIsShow" v-loading="humanIsLoading" class="absolute right-28 bottom-[250px] w-[303.75px] h-[540px]"> <span class="ywifont ywicon-guanbi text-[20px] cursor-pointer absolute top-2 right-2 z-[1]" @click="closeDigitalHuman"></span> <div class="duix-container h-full w-full"></div> </div> src/components/chat/components/playBar/hook/useDigitalHuman.ts
@@ -1,10 +1,12 @@ import { nextTick, onDeactivated, onMounted, ref } from 'vue'; import { SignJWT } from 'jose'; import { nextTick, onDeactivated, onMounted, ref } from 'vue'; import { markdownToTxt } from 'markdown-to-txt'; import './libs/duix.js'; import { questionStreamByPost } from '/@/api/ai/chat'; import { activeGroupType, activeRoomId } from '/@/stores/chatRoom'; import { markdownToTxt } from 'markdown-to-txt'; import axios from 'axios'; import { ElMessage } from 'element-plus'; export type UseDigitalHumanProps = { container: string; @@ -29,6 +31,32 @@ const closeDigitalHuman = () => { digitalHumanIsShow.value = false; resetDuixStatus(); }; /** * 检查数字人是否可用 */ const checkIsUseable = async () => { const config = { method: 'get', url: `https://duix.guiji.ai/duix-openapi-v2/v1/getconcurrentNumber?appId=${duixConfig.appId}`, headers: { priority: 'u=1, i', sig: duixConfig.sign, }, }; const response = await axios(config); const data = response.data.data; const total = data.totalConcurrentNumber; const user = data.userConcurrentNumber; if (total === null || total === 0) { return false; } if (total !== null && total === user) { return false; } return true; }; const resetDuixStatus = () => { @@ -59,6 +87,29 @@ }; let isWaitingSpeak = false; const speakContent = (content: string) => { // 打断之前的已收到xxx duix.break(); isWaitingSpeak = false; duix.speak({ content, }); }; const startDuix = () => { const conversationId = duixConfig.conversationId; // duix平台会话id duix .start({ conversationId, openAsr: true, wipeGreen: true }) .then((res) => { console.info('start', res); }) .catch((err) => { console.error('start error', err); }); }; const initDuix = () => { const sign = duixConfig.sign; // sign由服务端生成 const conversationId = duixConfig.conversationId; // duix平台会话id @@ -71,9 +122,7 @@ duix.on('intialSucccess', () => { console.info('intialSucccess'); // 此时初始化成功,可调用start duix.start({ conversationId, openAsr: true }).then((res) => { console.info('start', res); }); startDuix(); }); duix.on('bye', (data) => { console.info('bye', data); @@ -98,7 +147,10 @@ console.info('speakStart', data); }); duix.on('speakEnd', (data) => { isReceiveRes.value = false; if (!isWaitingSpeak) { isReceiveRes.value = false; duix.openAsr().then((...a) => {}); } }); duix.on('speakSection', (data) => { console.info('speakSection', data); @@ -108,18 +160,18 @@ }); duix.on('asrResult', (data) => { console.info('asrResult', data); if (isReceiveRes.value) { return; } duix.closeAsr().then((...a) => {}); let hasResult = false; isReceiveRes.value = true; try { // isWaitingSpeak = true; // duix.speak({ // content: '已收到您的问题,正在思考中...请稍等', // }); isWaitingSpeak = true; duix.speak({ content: '已收到您的问题,正在思考中...请稍等', }); questionStreamByPost( { question: data, @@ -132,26 +184,16 @@ if (chunkRes.mode === 'result' && chunkRes.value?.answer_type === 'knowledge') { const plainText = getPlainText(chunkRes.value); hasResult = true; duix.speak({ content: plainText, }); } if (!chunkRes.value?.json_ok && chunkRes.value?.err_code === 'MESSAGE') { speakContent(plainText); } else if (!chunkRes.value?.json_ok && chunkRes.value?.err_code === 'MESSAGE') { if (hasResult) return; hasResult = true; isWaitingSpeak = false; duix.speak({ content: chunkRes.value.json_msg, }); speakContent(chunkRes.value.json_msg); } if (chunkRes.mode === 'finish') { if (!hasResult) { isWaitingSpeak = false; duix.speak({ content: '暂时无法口头描述你所说的问题', }); speakContent('暂时无法口头描述你所说的问题'); } else { hasResult = false; } @@ -179,20 +221,23 @@ let hasInitDuix = false; let duix: any; const openDigitalHuman = () => { const openDigitalHuman = async () => { duixConfig.sign = await createSig(duixConfig.appId, duixConfig.appKey, 60 * 60 * duixConfig.expired); const isUsable = await checkIsUseable(); if (!isUsable) { ElMessage.warning('"资源占用中,请检查后再试~"'); return; } digitalHumanIsShow.value = true; nextTick(async () => { duixConfig.sign = await createSig(duixConfig.appId, duixConfig.appKey, 60 * 60 * duixConfig.expired); nextTick(() => { if (!hasInitDuix) { hasInitDuix = true; duix = new DUIX(); initDuix(); } else { duix.start({ conversationId: duixConfig.conversationId, openAsr: true }).then((res) => { console.info('start', res); }); startDuix(); } }); }; src/layout/component/header/Header.vue
@@ -2,19 +2,10 @@ <div class="top_text flex justify-between px-6 items-center pl-[unset] pr-6"> <div class="flex-items-center h-full"> <div class="nav-menu"> <router-link :to="firstToPath" class="nav-item" active-class="active"> <router-link v-for="item in menuList" :to="item.path" :key="item.label" class="nav-item" active-class="active"> <i class="icon-park-outline-robot"></i> 智能助手 {{ item.label }} </router-link> <!-- <router-link to="/workspace/situation" class="nav-item" active-class="active"> <i class="icon-park-outline-workbench"></i> 个人工作台 </router-link> <!-- <router-link to="/gis/situation" class="nav-item" active-class="active"> <i class="icon-park-outline-system"></i> GIS系统 </router-link> --> </div> </div> <el-dialog @@ -48,9 +39,18 @@ import { systemNotifyList } from '/@/api/ai/chat'; import router from '/@/router'; import pinia from '/@/stores'; import { activeChatRoom, newChatRoomClick, sidebarIsShow } from '/@/stores/chatRoom'; import { ParentRegister } from '/@/stores/global'; import { activeChatRoom, activeRoomId, activeTopMenuStyle, isSharePage, newChatRoomClick, TopMenuStyle, sidebarIsShow, } from '/@/stores/chatRoom'; import { useThemeConfig } from '/@/stores/themeConfig'; import { ParentRegister } from '/@/stores/global'; import emitter from '/@/utils/mitt'; import { userInfoKey } from '/@/utils/request'; import { Local } from '/@/utils/storage'; @@ -64,6 +64,34 @@ announcementTime: '', }); const menuList = computed(() => { const smartAssistant = { path: firstToPath.value, label: '智能助手', }; const personalWorkbench = { path: '/workspace/situation', label: '个人工作台', }; const gis = { path: '/gis/situation', label: 'GIS系统', }; return [smartAssistant]; switch (activeTopMenuStyle.value) { case TopMenuStyle.Normal: return [smartAssistant, personalWorkbench]; case TopMenuStyle.Gis: return [smartAssistant, gis]; default: return [smartAssistant]; } }); const smallScreenClick = () => { const pathname = window.location.pathname; const basePath = pathname.replace(/\/web\/index\.html$/, ''); src/stores/chatRoom.ts
@@ -304,3 +304,11 @@ isLoginStatus.value = false; LoginInfo.remove(); }; /** @description 顶部菜单布局方式 */ export const enum TopMenuStyle { Normal = 'normal', Gis = 'gis', } export const activeTopMenuStyle = ref<TopMenuStyle>(TopMenuStyle.Normal);