dd58c1d3a27ba48a5df050aab7c586bb9b988914..b9d28bd6af15026741099c25a6e72fe947ad3772
2025-04-10 wujingjing
修改数字人对话
b9d28b 对比 | 目录
2025-04-10 wujingjing
digitalHumanWidth
186586 对比 | 目录
2025-04-10 wujingjing
dotenv
bf9fb7 对比 | 目录
已修改8个文件
已添加1个文件
234 ■■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/helper.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/Chat.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/ChatContainer.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/components/playBar/hook/useDigitalHuman.ts 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/hooks/useAssistantContentOpt.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/messageList/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/types.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -97,6 +97,7 @@
        "@vue/compiler-sfc": "^3.4.15",
        "autoprefixer": "^10.4.19",
        "code-inspector-plugin": "^0.20.3",
        "dotenv": "^16.4.7",
        "eslint": "^8.35.0",
        "eslint-plugin-vue": "^9.9.0",
        "fs-extra": "^11.2.0",
scripts/helper.js
@@ -3,6 +3,7 @@
const os = require('os');
const path = require('path');
const chalk = require('chalk');
require('dotenv').config({ path: '.env.local' });
// èŽ·å–å½“å‰è„šæœ¬æ‰€åœ¨çš„ç›®å½•
const scriptDir = __dirname;
@@ -10,18 +11,19 @@
const argv2 = process.argv[2];
const customerList = argv2?.split(' ') ?? '';
const publicDir = path.join(rootDir, 'public');
const distDir = path.join(rootDir, 'dist');
// process.env.VITE_OUTPUT_DIR ||
const distDir =  path.join(rootDir, 'dist');
const customerListDir = path.join(rootDir, 'customer_list');
const customerProjectListDir = path.join(rootDir, 'src', 'views', 'project');
const firstCustomerName = customerList[0]?.split(':')[0];
/** å…¬å…±æ–‡ä»¶å¤¹ï¼Œæ‰€æœ‰å®¢æˆ·æ–‡ä»¶å¤¹å…±äº«æ–‡ä»¶ */
const commonDir = path.join(customerListDir, 'common');
 const item = customerList[0];
const item = customerList[0];
const customerSplit = item?.split(':');
const deployEnv = customerSplit?.[1];
// æ˜¯å¦ä¸ºç”Ÿäº§çŽ¯å¢ƒ
const isPro = deployEnv==='pro';
const isPro = deployEnv === 'pro';
// const deployEnv = process.argv[3];
const logColor = (text, color) => {
@@ -72,7 +74,7 @@
 */
const checkCustomer = (command, customer = firstCustomerName) => {
    if (!customer) {
        console.error(chalk.red(`请正确使用命令 â€œ${command} [customer]” `));
        console.error(chalk.red(`请正确使用命令 "${command} [customer]"`));
        exit(); // é€€å‡ºè„šæœ¬
    }
@@ -238,7 +240,7 @@
        checkFileExist(deployJSON, '配置文件');
        // è¯»å– JSON æ–‡ä»¶
        const config = await fs.readJson(deployJSON).catch((err) => {
            console.error(`读取配置文件“${deployJSON}”出错:`, err);
            console.error(`读取配置文件"${deployJSON}"出错:`, err);
            exit();
        });
@@ -293,7 +295,7 @@
                    }
                }
            } catch (error) {
                console.error(`读取“${customerFolder}”失败`, error);
                console.error(`读取"${customerFolder}"失败`, error);
            }
            try {
@@ -323,7 +325,7 @@
/**
 * åˆ‡æ¢åˆ†æ”¯
 */
const changeBranch = () =>{
const changeBranch = () => {
    return;
    if (isPro) {
        try {
@@ -334,7 +336,7 @@
            execSync('git checkout test', { stdio: 'inherit' });
        } catch (error) {}
    }
}
};
module.exports = {
    isPro,
@@ -368,5 +370,5 @@
    updateImportGlob,
    restoreImportGlob,
    deployEnv,
    changeBranch
    changeBranch,
};
src/components/chat/Chat.vue
@@ -1,5 +1,11 @@
<template>
    <ChatContainer :loading="chatListLoading" :more-is-loading="moreIsLoading" :is-share-page="isSharePage" ref="containerRef">
    <ChatContainer
        :loading="chatListLoading"
        :more-is-loading="moreIsLoading"
        :is-share-page="isSharePage"
        ref="containerRef"
        @autoSendMessage="autoSendMessage"
    >
        <!-- æ¶ˆæ¯åˆ—表 -->
        <template #message-list>
            <MessageList
@@ -54,6 +60,7 @@
import { computed, nextTick, onActivated, onMounted, ref } from 'vue';
import { loadAmisSource } from '../amis/load';
import ChatContainer from './components/ChatContainer.vue';
import { getKnowledgePlainText } from './components/playBar/hook/useDigitalHuman';
import ShareLinkDlg from './components/shareLink/index.vue';
import type { SendMsg } from './hooks/types';
import { useLoadData } from './hooks/useLoadData';
@@ -62,6 +69,7 @@
import MessageList from './messageList/index.vue';
import type { ChatContent } from './model/types';
import { AnswerState, AnswerType, RoleEnum, type ChatMessage } from './model/types';
import type { QuestionLifecycle } from './types';
import { getShareChatJsonByPost, questionStreamByPost } from '/@/api/ai/chat';
import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
import CustomDrawer from '/@/components/drawer/CustomDrawer.vue';
@@ -131,7 +139,8 @@
let streamOutputIsStart = false;
let position: Position = null;
const questionAi = async (text) => {
const questionAi = async (text: string, lifecycleCall?: QuestionLifecycle) => {
    let judgeParams = null;
    if (!preQuestion.value) {
        judgeParams = {};
@@ -211,6 +220,9 @@
                if (chunkRes.mode === 'result') {
                    lastIsResult = true;
                    const res = chunkRes.value;
                    if (chunkRes.value?.answer_type === 'knowledge') {
                        lifecycleCall?.receiveText?.(getKnowledgePlainText(chunkRes.value));
                    }
                    if (checkReportEmpty()) {
                        const resReport = getResReport();
                        resReport.reports.push(res);
@@ -364,6 +376,7 @@
                    stepList.at(-1).ms = ms;
                    isTalking.value = false;
                    streamOutputIsStart = false;
                    lifecycleCall?.finish?.();
                    return;
                }
@@ -546,7 +559,7 @@
        scrollToBottom();
    }, 300);
};
const sendChatMessage = async (content: ChatContent = messageContent.value) => {
const sendChatMessage = async (content: ChatContent = messageContent.value, lifecycleCall?: QuestionLifecycle) => {
    if (!checkCanSend(content)) {
        return;
    }
@@ -565,7 +578,7 @@
    try {
        const [userItem, assistantItem] = addChatItem(content);
        resMsgContent = await questionAi(content.values);
        resMsgContent = await questionAi(content.values, lifecycleCall);
        handleAfterQuestion(userItem, assistantItem, resMsgContent, {
            historyId: questionRes?.history_id,
            question: questionRes?.question,
@@ -661,6 +674,11 @@
    sendChatMessage(messageContent.value);
};
const autoSendMessage = (question: string, lifecycleCall?: QuestionLifecycle) => {
    messageContent.value.values = question;
    sendChatMessage(messageContent.value, lifecycleCall);
};
const { loadRangeData, onChatListScroll, moreIsLoading, updateLoadIndex } = useScrollLoad({
    container: chatListDom,
    historyGroupId: currentRouteId,
src/components/chat/components/ChatContainer.vue
@@ -22,12 +22,17 @@
                    <i class="ywifont ywicon-xiangxiajiantou !text-[20px]" />
                </div>
            </div>
            <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
                v-show="digitalHumanIsShow"
                v-loading="humanIsLoading"
                class="absolute right-0 bottom-0 z-[2]"
                :style="{ width: digitalHumanWidth, height: `calc(${digitalHumanWidth} * 16 / 9)` }"
            >
                <span class="ywifont ywicon-guanbi text-[20px] cursor-pointer absolute top-7 right-7 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]">
            <div v-if="!digitalHumanIsShow" class="absolute right-[24px] bottom-[100px] z-[2]">
                <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"
@@ -64,19 +69,28 @@
import { onActivated, onDeactivated, ref } from 'vue';
import { useChatWidth } from '../hooks/useChatWidth';
import { useScroll } from '../hooks/useScroll';
import emitter from '/@/utils/mitt';
import type { QuestionLifecycle } from '../types';
import { useDigitalHuman } from './playBar/hook/useDigitalHuman';
import emitter from '/@/utils/mitt';
const props = defineProps<{
    loading?: boolean;
    moreIsLoading?: boolean;
    isSharePage?: boolean;
}>();
const emit = defineEmits<{
    autoSendMessage: [string, QuestionLifecycle];
}>();
const chatListDom = ref<HTMLDivElement>();
const { openDigitalHuman, isHumanTalking, humanIsLoading,digitalHumanIsShow, closeDigitalHuman } = useDigitalHuman({
    container: '.duix-container',
});
const { openDigitalHuman, isHumanTalking, humanIsLoading, digitalHumanIsShow, closeDigitalHuman, digitalHumanWidth } = useDigitalHuman(
    {
        container: '.duix-container',
        autoSendMessage: (question: string, lifecycleCall?: QuestionLifecycle) => {
            emit('autoSendMessage', question, lifecycleCall);
        },
    }
);
const { scrollToBottom, isBottom } = useScroll({
    chatListDom,
});
src/components/chat/components/playBar/hook/useDigitalHuman.ts
@@ -1,19 +1,41 @@
import { SignJWT } from 'jose';
import { nextTick, onDeactivated, onMounted, ref } from 'vue';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { markdownToTxt } from 'markdown-to-txt';
import './libs/duix.js';
import { questionStreamByPost } from '/@/api/ai/chat';
import { activeGroupType, activeRoomId } from '/@/stores/chatRoom';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import type { QuestionLifecycle } from '../../../types';
export type UseDigitalHumanProps = {
    container: string;
    autoSendMessage: (question: string, lifecycleCall?: QuestionLifecycle) => void;
};
export const getKnowledgePlainText = (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;
    }, '');
    // const conclusionText =
    //     item.conclusion
    //         ?.filter((item) => !!item.report)
    //         .map((item) => item.report)
    //         .join('\n\n') ?? '';
    // result += knowledgeText + conclusionText;
    result = knowledgeText;
    return markdownToTxt(result);
};
export const useDigitalHuman = (props: UseDigitalHumanProps) => {
    const { container } = props;
    const { container, autoSendMessage } = props;
    const digitalHumanWidth = '240px';
    const duixConfig = {
        appId: '1356792813207031808',
        appKey: '659b068e-900c-4fe5-bb96-3ca70fe0aae4',
@@ -65,25 +87,6 @@
        // 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;
        }, '');
        // 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;
@@ -165,44 +168,28 @@
            }
            duix.closeAsr().then((...a) => {});
            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;
                            speakContent(plainText);
                        } else if (!chunkRes.value?.json_ok && chunkRes.value?.err_code === 'MESSAGE') {
                            if (hasResult) return;
                            hasResult = true;
                            speakContent(chunkRes.value.json_msg);
                        }
            isWaitingSpeak = true;
            duix.speak({
                content: '已收到您的问题,正在思考中...请稍等',
            });
                        if (chunkRes.mode === 'finish') {
                            if (!hasResult) {
                                speakContent('暂时无法口头描述你所说的问题');
                            } else {
                                hasResult = false;
                            }
                            // isReceiveRes.value = false;
            let content = '';
            try {
                autoSendMessage(data, {
                    receiveText: (text: string) => {
                        content += text;
                    },
                    finish: () => {
                        if (!content) {
                            speakContent('暂时无法口头描述你所说的问题');
                        } else {
                            speakContent(content);
                        }
                    }
                );
                    },
                });
            } catch (error) {
                console.error(error);
                isReceiveRes.value = false;
            }
        });
@@ -270,5 +257,6 @@
        isHumanTalking: isReceiveRes,
        closeDigitalHuman,
        humanIsLoading,
        digitalHumanWidth,
    };
};
src/components/chat/hooks/useAssistantContentOpt.ts
@@ -26,29 +26,39 @@
    };
    const checkIsText = (item) => {
        const isText = item?.content?.values?.some((item) => item?.content?.type === AnswerType.Knowledge)  || item?.conclusion?.length > 0;
        const isText =
            item?.content?.values?.some((item) => item?.content?.type === AnswerType.Knowledge || item?.conclusion?.length > 0) ||
            item?.conclusion?.length > 0;
        return isText;
    };
    const getPlainText = (item) => {
        let result = '';
        const knowledgeText = item.content.values
            .filter((item) => {
                const type = item?.content?.type;
                return type === AnswerType.Knowledge;
            })
            .reduce((acc, cur) => {
                const answer = cur?.content?.values
                    ?.map((item) => {
                        const mdText = item.answer;
                        const linkText = item.metadata?.Title;
                        if (linkText) {
                            return `${mdText}\n\n${linkText}`;
                        }
                        return mdText;
                    })
                    .join('\n\n');
                return acc + answer;
                const conclusionText =
                    cur.conclusion
                        ?.filter((cur) => !!cur.report)
                        .map((cur) => cur.report)
                        .join('\n\n') ?? '';
                acc += conclusionText;
                if (cur?.content?.type === AnswerType.Knowledge) {
                    const answer = cur?.content?.values
                        ?.map((item) => {
                            const mdText = item.answer;
                            const linkText = item.metadata?.Title;
                            if (linkText) {
                                return `${mdText}\n\n${linkText}`;
                            }
                            return mdText;
                        })
                        .join('\n\n');
                    acc += answer;
                }
                return acc;
            }, '');
        const conclusionText =
src/components/chat/messageList/index.vue
@@ -19,7 +19,7 @@
                :isTalking="isTalking"
            />
        </div>
        <div v-if="showAskMore" class="ml-4 mt-5 ">
        <div v-if="showAskMore && msgList.at(-1)?.content?.askMoreList?.length > 0" class="ml-4 mt-5 ">
            <div class="text-gray-600 mb-5">你可以继续问我:</div>
            <div class="space-y-2 inline-flex flex-col">
                <div
src/components/chat/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
export type QuestionLifecycle = {
    finish?: () => void;
    receiveText?: (text: string) => void;
};
vite.config.ts
@@ -1,11 +1,10 @@
import vue from '@vitejs/plugin-vue';
import autoprefixer from 'autoprefixer';
import { CodeInspectorPlugin } from 'code-inspector-plugin';
import { resolve } from 'path';
import tailwindcss from 'tailwindcss';
import type { ConfigEnv } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import { CodeInspectorPlugin } from 'code-inspector-plugin';
import { visualizer } from 'rollup-plugin-visualizer';
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
// import AutoImport from 'unplugin-auto-import/vite';
// import Components from 'unplugin-vue-components/vite';
@@ -81,8 +80,7 @@
            },
        },
        build: {
            // outDir: 'dist/' + mode.mode,
            outDir: 'dist',
            outDir:'dist',
            chunkSizeWarningLimit: 1500,
            rollupOptions: {