接入coze后渲染coze知识库+问题推荐

pull/824/head
阎文成 2025-09-25 19:28:36 +08:00
parent d076d75c38
commit 7e8b987653
5 changed files with 95 additions and 22 deletions

View File

@ -17,6 +17,7 @@ export interface ChatMessageVO {
attachmentUrls?: string[] // 附件 URL 数组 attachmentUrls?: string[] // 附件 URL 数组
tokens: number // 消耗 Token 数量 tokens: number // 消耗 Token 数量
segmentIds?: number[] // 段落编号 segmentIds?: number[] // 段落编号
followUps?:string[]//问题推荐
segments?: { segments?: {
id: number // 段落编号 id: number // 段落编号
content: string // 段落内容 content: string // 段落内容

View File

@ -0,0 +1,44 @@
<template>
<div v-if="followUps && followUps.length > 0" class="w-[100%]">
{{item}}
<div
class="follow"
@click="followClick(follow)"
v-for="(follow,index) in followUps"
:key="index"
>
{{follow}}
</div>
</div>
</template>
<script setup lang="ts">
import {ChatMessageVO} from "@/api/ai/chat/message";
import {cloneDeep} from "lodash-es";
const emits = defineEmits([ 'onRefresh']) // emits
defineOptions({ name: 'MessageFollowUps' })
defineProps<{
followUps?: string[]
}>()
/** 刷新 */
const followClick = async (follow:string) => {
emits('onRefresh', {content:follow})
}
</script>
<style scoped lang="scss">
.follow{
border: 1px solid #dcdfe6;
padding: 10px;
margin-bottom: 5px;
width: 100%;
color:#606266;
border-radius: 5px;
}
.follow:hover{
background-color: #dcdcdc;
cursor: pointer;
}
</style>

View File

@ -13,7 +13,9 @@
@click="handleClick(doc)" @click="handleClick(doc)"
> >
<div class="text-14px text-[#333] mb-4px"> <div class="text-14px text-[#333] mb-4px">
{{ doc.title }} {{ doc.title }}<el-link v-if="document?.url" :href="document?.url" target="_blank" type="primary">
下载
</el-link>
<span class="text-12px text-[#999] ml-4px">{{ doc.segments.length }} </span> <span class="text-12px text-[#999] ml-4px">{{ doc.segments.length }} </span>
</div> </div>
</div> </div>
@ -33,7 +35,9 @@
<div ref="documentRef"></div> <div ref="documentRef"></div>
</template> </template>
<template #default> <template #default>
<div class="text-16px font-bold mb-12px">{{ document?.title }}</div> <div class="text-16px font-bold mb-12px">{{ document?.title }}
</div>
<div class="max-h-[60vh] overflow-y-auto"> <div class="max-h-[60vh] overflow-y-auto">
<div <div
v-for="(segment, index) in document?.segments" v-for="(segment, index) in document?.segments"
@ -60,6 +64,7 @@ const props = defineProps<{
id: number id: number
documentId: number documentId: number
documentName: string documentName: string
documentUrl: string
content: string content: string
}[] }[]
}>() }>()
@ -70,6 +75,7 @@ const document = ref<{
segments: { segments: {
id: number id: number
content: string content: string
url: string
}[] }[]
} | null>(null) // } | null>(null) //
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
@ -85,12 +91,14 @@ const documentList = computed(() => {
docMap.set(segment.documentId, { docMap.set(segment.documentId, {
id: segment.documentId, id: segment.documentId,
title: segment.documentName, title: segment.documentName,
url:segment.documentUrl,
segments: [] segments: []
}) })
} }
docMap.get(segment.documentId).segments.push({ docMap.get(segment.documentId).segments.push({
id: segment.id, id: segment.id,
content: segment.content content: segment.content,
url:segment.documentUrl
}) })
}) })
return Array.from(docMap.values()) return Array.from(docMap.values())

View File

@ -23,6 +23,7 @@
:content="item.content" :content="item.content"
/> />
<MessageFiles :attachment-urls="item.attachmentUrls" /> <MessageFiles :attachment-urls="item.attachmentUrls" />
<MessageKnowledge v-if="item.segments" :segments="item.segments" /> <MessageKnowledge v-if="item.segments" :segments="item.segments" />
<MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" /> <MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
</div> </div>
@ -43,6 +44,10 @@
<img class="h-17px" src="@/assets/ai/delete.svg" /> <img class="h-17px" src="@/assets/ai/delete.svg" />
</el-button> </el-button>
</div> </div>
<div class="flex flex-row mt-8px" v-if="index==list.length-1">
<MessageFollowUps @on-refresh="onRefresh" :followUps="item.followUps"/>
</div>
</div> </div>
</div> </div>
<!-- 靠右 messageuser 类型 --> <!-- 靠右 messageuser 类型 -->
@ -114,6 +119,7 @@ import { PropType } from 'vue'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import MarkdownView from '@/components/MarkdownView/index.vue' import MarkdownView from '@/components/MarkdownView/index.vue'
import MessageKnowledge from './MessageKnowledge.vue' import MessageKnowledge from './MessageKnowledge.vue'
import MessageFollowUps from './MessageFollowUps.vue'
import MessageReasoning from './MessageReasoning.vue' import MessageReasoning from './MessageReasoning.vue'
import MessageFiles from './MessageFiles.vue' import MessageFiles from './MessageFiles.vue'
import MessageWebSearch from './MessageWebSearch.vue' import MessageWebSearch from './MessageWebSearch.vue'

View File

@ -21,7 +21,7 @@
<div class="flex w-300px flex-row justify-end" v-if="activeConversation"> <div class="flex w-300px flex-row justify-end" v-if="activeConversation">
<el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm"> <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
<span v-html="activeConversation?.modelName"></span> <span v-html="activeConversation?.modelName"></span>
<Icon icon="ep:setting" class="ml-10px" /> <Icon icon="ep:setting" class="ml-10px"/>
</el-button> </el-button>
<el-button size="small" class="p-10px" @click="handlerMessageClear"> <el-button size="small" class="p-10px" @click="handlerMessageClear">
<Icon <Icon
@ -30,10 +30,10 @@
/> />
</el-button> </el-button>
<el-button size="small" class="p-10px"> <el-button size="small" class="p-10px">
<Icon icon="ep:download" color="var(--el-text-color-placeholder)" /> <Icon icon="ep:download" color="var(--el-text-color-placeholder)"/>
</el-button> </el-button>
<el-button size="small" class="p-10px" @click="handleGoTopMessage"> <el-button size="small" class="p-10px" @click="handleGoTopMessage">
<Icon icon="ep:top" color="var(--el-text-color-placeholder)" /> <Icon icon="ep:top" color="var(--el-text-color-placeholder)"/>
</el-button> </el-button>
</div> </div>
</el-header> </el-header>
@ -43,7 +43,7 @@
<div> <div>
<div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0"> <div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
<!-- 情况一消息加载中 --> <!-- 情况一消息加载中 -->
<MessageLoading v-if="activeMessageListLoading" /> <MessageLoading v-if="activeMessageListLoading"/>
<!-- 情况二无聊天对话时 --> <!-- 情况二无聊天对话时 -->
<MessageNewConversation <MessageNewConversation
v-if="!activeConversation" v-if="!activeConversation"
@ -87,10 +87,10 @@
</textarea> </textarea>
<div class="flex justify-between pb-0 pt-5px"> <div class="flex justify-between pb-0 pt-5px">
<div class="flex items-center"> <div class="flex items-center">
<MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px" /> <MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px"/>
<el-switch v-model="enableContext" /> <el-switch v-model="enableContext"/>
<span class="ml-5px mr-15px text-14px text-#8f8f8f">上下文</span> <span class="ml-5px mr-15px text-14px text-#8f8f8f">上下文</span>
<el-switch v-model="enableWebSearch" /> <el-switch v-model="enableWebSearch"/>
<span class="ml-5px text-14px text-#8f8f8f">联网搜索</span> <span class="ml-5px text-14px text-#8f8f8f">联网搜索</span>
</div> </div>
<el-button <el-button
@ -124,8 +124,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' import {ChatMessageApi, ChatMessageVO} from '@/api/ai/chat/message'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
import ConversationList from './components/conversation/ConversationList.vue' import ConversationList from './components/conversation/ConversationList.vue'
import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue' import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue'
import MessageList from './components/message/MessageList.vue' import MessageList from './components/message/MessageList.vue'
@ -135,7 +135,7 @@ import MessageNewConversation from './components/message/MessageNewConversation.
import MessageFileUpload from './components/message/MessageFileUpload.vue' import MessageFileUpload from './components/message/MessageFileUpload.vue'
/** AI 聊天对话 列表 */ /** AI 聊天对话 列表 */
defineOptions({ name: 'AiChat' }) defineOptions({name: 'AiChat'})
const route = useRoute() // const route = useRoute() //
const message = useMessage() // const message = useMessage() //
@ -165,6 +165,7 @@ const enableWebSearch = ref<boolean>(false) // 是否开启联网搜索
const uploadFiles = ref<string[]>([]) // URL const uploadFiles = ref<string[]>([]) // URL
// Stream // Stream
const receiveMessageFullText = ref('') const receiveMessageFullText = ref('')
const receiveFollowUps = ref<string[]>([])//
const receiveMessageDisplayedText = ref('') const receiveMessageDisplayedText = ref('')
// =========== =========== // =========== ===========
@ -335,7 +336,8 @@ const handlerMessageClear = async () => {
await ChatMessageApi.deleteByConversationId(activeConversationId.value) await ChatMessageApi.deleteByConversationId(activeConversationId.value)
// message // message
activeMessageList.value = [] activeMessageList.value = []
} catch {} } catch {
}
} }
/** 回到 message 列表的顶部 */ /** 回到 message 列表的顶部 */
@ -439,7 +441,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
conversationInProgress.value = true conversationInProgress.value = true
// //
receiveMessageFullText.value = '' receiveMessageFullText.value = ''
receiveFollowUps.value = []
try { try {
// 1.1 stream // 1.1 stream
activeMessageList.value.push({ activeMessageList.value.push({
@ -473,7 +475,14 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
enableContext.value, enableContext.value,
enableWebSearch.value, enableWebSearch.value,
async (res) => { async (res) => {
const { code, data, msg } = JSON.parse(res.data)
const {code, data, msg} = JSON.parse(res.data)
if (data.receive.followUps.length>0) {
receiveFollowUps.value = data.receive.followUps
}
if (code !== 0) { if (code !== 0) {
message.alert(`对话异常! ${msg}`) message.alert(`对话异常! ${msg}`)
// //
@ -487,6 +496,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
if (data.receive.content === '' && !data.receive.reasoningContent) { if (data.receive.content === '' && !data.receive.reasoningContent) {
return return
} }
//
// message // message
if (isFirstChunk) { if (isFirstChunk) {
@ -497,16 +507,17 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
// //
activeMessageList.value.push(data.send) activeMessageList.value.push(data.send)
data.send.attachmentUrls = userMessage.attachmentUrls data.send.attachmentUrls = userMessage.attachmentUrls
activeMessageList.value.push(data.receive) activeMessageList.value.push(data.receive)
} }
// reasoningContent // reasoningContent
if (data.receive.reasoningContent) { if (data.receive.reasoningContent) {
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1] const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
lastMessage.reasoningContent = lastMessage.reasoningContent =
lastMessage.reasoningContent + data.receive.reasoningContent lastMessage.reasoningContent + data.receive.reasoningContent
} }
// //
if (data.receive.content !== '') { if (data.receive.content !== '') {
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
@ -526,7 +537,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
}, },
userMessage.attachmentUrls userMessage.attachmentUrls
) )
} catch {} } catch {
}
} }
/** 停止 stream 流式调用 */ /** 停止 stream 流式调用 */
@ -587,13 +599,14 @@ const textRoll = async () => {
if (!conversationInProgress.value) { if (!conversationInProgress.value) {
textSpeed.value = 10 textSpeed.value = 10
} }
// message
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
lastMessage.followUps = receiveFollowUps.value
if (index < receiveMessageFullText.value.length) { if (index < receiveMessageFullText.value.length) {
receiveMessageDisplayedText.value += receiveMessageFullText.value[index] receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
index++ index++
// message
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
lastMessage.content = receiveMessageDisplayedText.value lastMessage.content = receiveMessageDisplayedText.value
// //
await scrollToBottom() await scrollToBottom()
@ -611,7 +624,8 @@ const textRoll = async () => {
} }
} }
let timer = setTimeout(task, textSpeed.value) let timer = setTimeout(task, textSpeed.value)
} catch {} } catch {
}
} }
/** 初始化 **/ /** 初始化 **/