wujingjing
2025-04-07 07f5e49d4031f74c296fe01bb79e913c864979e5
数字人
已修改10个文件
已添加2个文件
20419 ■■■■■ 文件已修改
customer_list/common/static/fonts/ywiconfont/iconfont.css 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/common/static/fonts/ywiconfont/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/common/static/fonts/ywiconfont/iconfont.woff 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/common/static/fonts/ywiconfont/iconfont.woff2 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/ChatContainer.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/playBar/PlayBar.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/playBar/hook/libs/duix.js 20133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/playBar/hook/useDigitalHuman.ts 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useSyncMsg.ts 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/common/static/fonts/ywiconfont/iconfont.css
@@ -1,8 +1,8 @@
@font-face {
  font-family: "ywifont"; /* Project id 4655417 */
  src: url('iconfont.woff2?t=1742799969257') format('woff2'),
       url('iconfont.woff?t=1742799969257') format('woff'),
       url('iconfont.ttf?t=1742799969257') format('truetype');
  src: url('iconfont.woff2?t=1744017975539') format('woff2'),
       url('iconfont.woff?t=1744017975539') format('woff'),
       url('iconfont.ttf?t=1744017975539') format('truetype');
}
.ywifont {
@@ -13,6 +13,10 @@
  -moz-osx-font-smoothing: grayscale;
}
.ywicon-shuziren:before {
  content: "\e64e";
}
.ywicon-shengyin:before {
  content: "\e8b8";
}
customer_list/common/static/fonts/ywiconfont/iconfont.ttf
Binary files differ
customer_list/common/static/fonts/ywiconfont/iconfont.woff
Binary files differ
customer_list/common/static/fonts/ywiconfont/iconfont.woff2
Binary files differ
package-lock.json
@@ -37,6 +37,7 @@
                "file-saver": "^2.0.5",
                "highlight.js": "^11.7.0",
                "html2canvas": "^1.4.1",
                "jose": "^6.0.10",
                "jquery": "^3.7.1",
                "js-cookie": "^3.0.1",
                "js-table2excel": "^1.0.3",
@@ -5886,6 +5887,15 @@
            "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
            "bin": {
                "jiti": "bin/jiti.js"
            }
        },
        "node_modules/jose": {
            "version": "6.0.10",
            "resolved": "https://registry.npmmirror.com/jose/-/jose-6.0.10.tgz",
            "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==",
            "license": "MIT",
            "funding": {
                "url": "https://github.com/sponsors/panva"
            }
        },
        "node_modules/jquery": {
@@ -14643,6 +14653,11 @@
            "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.6.tgz",
            "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="
        },
        "jose": {
            "version": "6.0.10",
            "resolved": "https://registry.npmmirror.com/jose/-/jose-6.0.10.tgz",
            "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw=="
        },
        "jquery": {
            "version": "3.7.1",
            "resolved": "https://registry.npmmirror.com/jquery/-/jquery-3.7.1.tgz",
package.json
@@ -54,6 +54,7 @@
        "file-saver": "^2.0.5",
        "highlight.js": "^11.7.0",
        "html2canvas": "^1.4.1",
        "jose": "^6.0.10",
        "jquery": "^3.7.1",
        "js-cookie": "^3.0.1",
        "js-table2excel": "^1.0.3",
src/components/chat/components/ChatContainer.vue
@@ -22,6 +22,21 @@
                    <i class="ywifont ywicon-xiangxiajiantou !text-[20px]" />
                </div>
            </div>
            <div v-show="digitalHumanIsShow" class="absolute right-28 bottom-[250px] w-[400px] 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>
            <div v-if="!digitalHumanIsShow" class="absolute right-28 bottom-[250px]">
                <el-tooltip content="数字人" placement="top">
                    <div
                        class="flex items-center justify-center size-[38px] cursor-pointer hover:text-[#0284ff] border rounded-full hover:bg-[#f6f7f9] shadow bg-white"
                        @click="openDigitalHuman"
                    >
                        <i class="ywifont ywicon-shuziren !text-[20px]" />
                    </div>
                </el-tooltip>
            </div>
            <!-- è¾“入区域 -->
            <div class="w-full px-6 pb-6 bg-[rgb(247,248,250)] flex justify-center z-[1] flex-0" v-if="!isSharePage">
@@ -50,6 +65,7 @@
import { useChatWidth } from '../hooks/useChatWidth';
import { useScroll } from '../hooks/useScroll';
import emitter from '/@/utils/mitt';
import { useDigitalHuman } from './playBar/hook/useDigitalHuman';
const props = defineProps<{
    loading?: boolean;
@@ -58,7 +74,9 @@
}>();
const chatListDom = ref<HTMLDivElement>();
const { openDigitalHuman, isHumanTalking, digitalHumanIsShow, closeDigitalHuman } = useDigitalHuman({
    container: '.duix-container',
});
const { scrollToBottom, isBottom } = useScroll({
    chatListDom,
});
src/components/chat/components/playBar/PlayBar.vue
@@ -145,6 +145,7 @@
                                    <span class="ywifont ywicon-fujian !text-[20px] font-bold"></span>
                                </div>
                            </el-tooltip>
                            <el-tooltip v-if="isSupportSpeech" placement="top" :content="recordState.isRecording ? '停止语音输入' : '语音输入'">
                                <div class="cursor-pointer size-[24px] relative !z-10 rounded flex-center hover:bg-[#f2f2f2]" @click="speechClick">
@@ -220,6 +221,7 @@
import MetricValuesPreview from './metricValues/MetricValuesPreview.vue';
import { useSpeech } from './hook/useSpeech';
import { useDigitalHuman } from './hook/useDigitalHuman';
const emits = defineEmits(['sendClick', 'stopGenClick']);
const props = defineProps({
@@ -256,7 +258,9 @@
    inputValue: inputValue,
    inputRef: inputRef,
});
const { openDigitalHuman, isHumanTalking, digitalHumanIsShow } = useDigitalHuman({
    container: '.duix-container',
});
const clearTextarea = () => {
    inputValue.value = '';
};
@@ -324,6 +328,7 @@
    pastTarget: inputRef as any,
    attachFileList: attachList,
});
const deleteAttachInIndex = (index: number) => {
    const attach = attachList.value[index];
    if (attach.type === 'file') {
src/components/chat/components/playBar/hook/libs/duix.js
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
src/components/chat/components/playBar/hook/useDigitalHuman.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,226 @@
import { nextTick, onDeactivated, onMounted, ref } from 'vue';
import { SignJWT } from 'jose';
import './libs/duix.js';
import { questionStreamByPost } from '/@/api/ai/chat';
import { activeGroupType, activeRoomId } from '/@/stores/chatRoom';
import { markdownToTxt } from 'markdown-to-txt';
export type UseDigitalHumanProps = {
    container: string;
};
export const useDigitalHuman = (props: UseDigitalHumanProps) => {
    const { container } = props;
    const duixConfig = {
        appId: '1356792813207031808',
        appKey: '659b068e-900c-4fe5-bb96-3ca70fe0aae4',
        sign: '',
        conversationId: '1909088110274277378',
        /** @description è¿‡æœŸæ—¶é—´ï¼ˆå°æ—¶ï¼‰ */
        expired: 12,
    };
    // æ˜¯å¦å·²æŽ¥å£ç›¸åº”
    const isReceiveRes = ref(false);
    const digitalHumanIsShow = ref(false);
    const closeDigitalHuman = () => {
        digitalHumanIsShow.value = false;
        resetDuixStatus();
    };
    const resetDuixStatus = () => {
        isReceiveRes.value = false;
        // isSpeaking.value = false;
        digitalHumanIsShow.value = false;
        duix?.stop();
    };
    const getPlainText = (item) => {
        let result = '';
        const knowledgeText = item.knowledge.reduce((acc, cur) => {
            const mdText = cur.answer;
            const linkText = cur.metadata?.Title;
            if (linkText) {
                return `${mdText}\n\n${linkText}`;
            }
            return acc + mdText;
        }, '');
        console.log('🚀 ~ knowledgeText:', knowledgeText);
        // const conclusionText =
        //     item.conclusion
        //         ?.filter((item) => !!item.report)
        //         .map((item) => item.report)
        //         .join('\n\n') ?? '';
        // result += knowledgeText + conclusionText;
        result = knowledgeText;
        return markdownToTxt(result);
    };
    let isWaitingSpeak = false;
    const initDuix = () => {
        const sign = duixConfig.sign; // sign由服务端生成
        const conversationId = duixConfig.conversationId; // duix平台会话id
        if (!sign || !conversationId) {
            return alert('参数不能为空');
        }
        duix.on('error', (data) => {
            console.error(data);
        });
        duix.on('intialSucccess', () => {
            console.info('intialSucccess');
            // æ­¤æ—¶åˆå§‹åŒ–成功,可调用start
            duix.start({ conversationId, openAsr: true }).then((res) => {
                console.info('start', res);
            });
        });
        duix.on('bye', (data) => {
            console.info('bye', data);
        });
        duix.on('progress', (progress) => {
            console.info('progress', progress);
        });
        duix.on('show', () => {
            console.info('show');
            // æ­¤æ—¶å¯ç¡®è®¤è§†é¢‘å·²
            // (document.querySelector('#modal') as HTMLElement).style.display = 'none';
        });
        duix.on('openAsrSuccess', () => {
            console.info('openAsrSuccess');
        });
        duix.on('asrClose', () => {
            console.info('asrClose');
        });
        duix.on('speakStart', (data) => {
            // isSpeaking.value = true;
            console.info('speakStart', data);
        });
        duix.on('speakEnd', (data) => {
            isReceiveRes.value = false;
        });
        duix.on('speakSection', (data) => {
            console.info('speakSection', data);
        });
        duix.on('speakError', (data) => {
            console.info('speakError', data);
        });
        duix.on('asrResult', (data) => {
            console.info('asrResult', data);
            if (isReceiveRes.value) {
                return;
            }
            let hasResult = false;
            isReceiveRes.value = true;
            try {
                // isWaitingSpeak = true;
                // duix.speak({
                //     content: '已收到您的问题,正在思考中...请稍等',
                // });
                questionStreamByPost(
                    {
                        question: data,
                        history_group_id: activeRoomId.value,
                        raw_mode: false,
                        group_type: activeGroupType.value,
                        is_digital_human: true,
                    },
                    (chunkRes) => {
                        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') {
                            hasResult = true;
                            isWaitingSpeak = false;
                            duix.speak({
                                content: chunkRes.value.json_msg,
                            });
                        }
                        if (chunkRes.mode === 'finish') {
                            if (!hasResult) {
                                isWaitingSpeak = false;
                                duix.speak({
                                    content: '暂时无法口头描述你所说的问题',
                                });
                            } else {
                                hasResult = false;
                            }
                            // isReceiveRes.value = false;
                        }
                    }
                );
            } catch (error) {
                console.error(error);
                isReceiveRes.value = false;
            }
        });
        duix.on('report', (data) => {
            // console.info('report', data)
        });
        duix
            .init({
                sign,
                containerLable: container,
            })
            .then((data) => {
                console.info('init', data);
            });
    };
    let hasInitDuix = false;
    let duix: any;
    const openDigitalHuman = () => {
        digitalHumanIsShow.value = true;
        nextTick(async () => {
            duixConfig.sign = await createSig(duixConfig.appId, duixConfig.appKey, 60 * 60 * duixConfig.expired);
            if (!hasInitDuix) {
                hasInitDuix = true;
                duix = new DUIX();
                initDuix();
            } else {
                duix.start({ conversationId: duixConfig.conversationId, openAsr: true }).then((res) => {
                    console.info('start', res);
                });
            }
        });
    };
    function createSig(appId, appKey, sigExp) {
        const now = Math.floor(Date.now() / 1000);
        const expiresAt = now + sigExp;
        const sign = new SignJWT({ appId })
            .setProtectedHeader({ alg: 'HS256' })
            .setIssuedAt(now)
            .setExpirationTime(expiresAt)
            .sign(new TextEncoder().encode(appKey));
        return sign;
    }
    onMounted(() => {
        window.addEventListener('beforeunload', () => {
            closeDigitalHuman();
        });
    });
    onDeactivated(() => {
        closeDigitalHuman();
    });
    return {
        digitalHumanIsShow,
        openDigitalHuman,
        isHumanTalking: isReceiveRes,
        closeDigitalHuman,
    };
};
src/components/chat/hooks/useSyncMsg.ts
@@ -52,7 +52,10 @@
            const groupId = unref(historyGroupId);
            const startGroupId = data?.history_group_id;
            if (groupId !== startGroupId) return;
            showTip(data);
            const isDigitalHuman = data?.is_digital_human;
            if (!isDigitalHuman) {
                showTip(data);
            }
        }
        if (data?.type === 'chat_history_id') {
            const groupId = unref(historyGroupId);
vite.config.ts
@@ -58,7 +58,7 @@
            host: '0.0.0.0',
            port: env.VITE_PORT as unknown as number,
            open: JSON.parse(env.VITE_OPEN),
            hmr: false,
            hmr: true,
            proxy: {
                '/events': {
                    target: 'http://localhost:3000',