feat:【ai 大模型】增加联网搜索功能

pull/788/MERGE
YunaiV 2025-08-25 23:47:47 +08:00
parent b8a1fbcb14
commit 93e3428982
4 changed files with 207 additions and 1 deletions

View File

@ -23,6 +23,14 @@ export interface ChatMessageVO {
documentId: number // 文档编号
documentName: string // 文档名称
}[]
webSearchPages?: {
name: string // 名称
icon: string // 图标
title: string // 标题
url: string // URL
snippet: string // 内容的简短描述
summary: string // 内容的文本摘要
}[]
createTime: Date // 创建时间
roleAvatar: string // 角色头像
userAvatar: string // 用户头像
@ -44,6 +52,7 @@ export const ChatMessageApi = {
content: string,
ctrl,
enableContext: boolean,
enableWebSearch: boolean,
onMessage,
onError,
onClose,
@ -61,6 +70,7 @@ export const ChatMessageApi = {
conversationId,
content,
useContext: enableContext,
webSearch: enableWebSearch,
attachmentUrls: attachmentUrls || []
}),
onmessage: onMessage,

View File

@ -24,6 +24,7 @@
/>
<MessageFiles :attachment-urls="item.attachmentUrls" />
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
<MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
</div>
<div class="flex flex-row mt-8px">
<el-button
@ -115,6 +116,7 @@ import MarkdownView from '@/components/MarkdownView/index.vue'
import MessageKnowledge from './MessageKnowledge.vue'
import MessageReasoning from './MessageReasoning.vue'
import MessageFiles from './MessageFiles.vue'
import MessageWebSearch from './MessageWebSearch.vue'
import { useClipboard } from '@vueuse/core'
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'

View File

@ -0,0 +1,190 @@
<!-- 联网搜索结果组件 -->
<template>
<!-- 联网搜索结果列表 -->
<div
v-if="webSearchPages && webSearchPages.length > 0"
class="mt-10px p-10px rounded-8px bg-[#f5f5f5]"
>
<!-- 标题栏可点击展开/收起 -->
<div
class="text-14px text-[#666] mb-8px flex items-center justify-between cursor-pointer hover:text-[#409eff]"
@click="toggleExpanded"
>
<div class="flex items-center">
<Icon icon="ep:search" class="mr-5px" />
联网搜索结果 ({{ webSearchPages.length }} )
</div>
<Icon
:icon="isExpanded ? 'ep:arrow-up' : 'ep:arrow-down'"
class="text-12px transition-transform duration-200"
/>
</div>
<!-- 可展开的搜索结果列表 -->
<div v-show="isExpanded" class="flex flex-col gap-8px transition-all duration-200 ease-in-out">
<div
v-for="(result, index) in webSearchPages"
:key="index"
class="p-10px bg-white rounded-6px cursor-pointer transition-all hover:bg-[#e6f4ff]"
@click="handleClick(result)"
>
<div class="flex items-start gap-8px">
<!-- 网站图标 -->
<div class="flex-shrink-0 w-16px h-16px mt-2px">
<img
v-if="result.icon"
:src="result.icon"
:alt="result.name"
class="w-full h-full object-contain rounded-2px"
@error="handleImageError"
/>
<Icon v-else icon="ep:link" class="w-full h-full text-[#666]" />
</div>
<!-- 内容区域 -->
<div class="flex-1 min-w-0">
<!-- 标题和来源 -->
<div class="flex items-center gap-4px mb-4px">
<span class="text-12px text-[#999] truncate">{{ result.name }}</span>
</div>
<!-- 主标题 -->
<div class="text-14px text-[#1a73e8] font-medium mb-4px line-clamp-2 leading-[1.4]">
{{ result.title }}
</div>
<!-- 描述 -->
<div class="text-13px text-[#666] line-clamp-2 leading-[1.4] mb-4px">
{{ result.snippet }}
</div>
<!-- URL -->
<div class="text-12px text-[#006621] truncate">
{{ result.url }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 联网搜索详情弹窗 -->
<el-popover
v-model:visible="dialogVisible"
:width="600"
trigger="click"
placement="top-start"
:offset="55"
popper-class="web-search-popover"
>
<template #reference>
<div ref="resultRef"></div>
</template>
<template #default>
<div v-if="selectedResult">
<!-- 标题区域 -->
<div class="flex items-start gap-8px mb-12px">
<div class="flex-shrink-0 w-20px h-20px mt-2px">
<img
v-if="selectedResult.icon"
:src="selectedResult.icon"
:alt="selectedResult.name"
class="w-full h-full object-contain rounded-2px"
@error="handleImageError"
/>
<Icon v-else icon="ep:link" class="w-full h-full text-[#666]" />
</div>
<div class="flex-1 min-w-0">
<div class="text-16px font-bold text-[#333] mb-4px line-clamp-2">
{{ selectedResult.title }}
</div>
<div class="text-12px text-[#999] mb-4px">{{ selectedResult.name }}</div>
<div class="text-12px text-[#006621] break-all">{{ selectedResult.url }}</div>
</div>
</div>
<!-- 内容区域 -->
<div class="max-h-[60vh] overflow-y-auto">
<!-- 简短描述 -->
<div class="mb-12px">
<div class="text-14px font-medium text-[#333] mb-6px">简短描述</div>
<div class="text-14px leading-[1.6] text-[#666] bg-[#f8f9fa] p-10px rounded-6px">
{{ selectedResult.snippet }}
</div>
</div>
<!-- 内容摘要 -->
<div v-if="selectedResult.summary">
<div class="text-14px font-medium text-[#333] mb-6px">内容摘要</div>
<div
class="text-14px leading-[1.6] text-[#333] bg-[#f8f9fa] p-10px rounded-6px whitespace-pre-wrap"
>
{{ selectedResult.summary }}
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-end gap-8px mt-12px pt-12px border-t border-[#eee]">
<el-button size="small" @click="dialogVisible = false">关闭</el-button>
<el-button type="primary" size="small" @click="openUrl(selectedResult.url)">
访问原文
</el-button>
</div>
</div>
</template>
</el-popover>
</template>
<script setup lang="ts">
defineProps<{
webSearchPages: {
name: string //
icon: string //
title: string //
url: string // URL
snippet: string //
summary: string //
}[]
}>()
const isExpanded = ref(false) //
const selectedResult = ref<{
name: string
icon: string
title: string
url: string
snippet: string
summary: string
} | null>(null) //
const dialogVisible = ref(false) //
const resultRef = ref<HTMLElement>() // Ref
/** 切换展开/收起状态 */
const toggleExpanded = () => {
isExpanded.value = !isExpanded.value
}
/** 点击搜索结果处理 */
const handleClick = (result: any) => {
selectedResult.value = result
dialogVisible.value = true
}
/** 处理图片加载错误 */
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'none'
}
/** 打开URL */
const openUrl = (url: string) => {
window.open(url, '_blank')
}
</script>
<style scoped>
.web-search-popover {
max-width: 600px;
}
</style>

View File

@ -89,7 +89,9 @@
<div class="flex items-center">
<MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px" />
<el-switch v-model="enableContext" />
<span class="ml-5px text-14px text-#8f8f8f">上下文</span>
<span class="ml-5px mr-15px text-14px text-#8f8f8f">上下文</span>
<el-switch v-model="enableWebSearch" />
<span class="ml-5px text-14px text-#8f8f8f">联网搜索</span>
</div>
<el-button
type="primary"
@ -159,6 +161,7 @@ const conversationInAbortController = ref<any>() // 对话进行中 abort 控制
const inputTimeout = ref<any>() //
const prompt = ref<string>() // prompt
const enableContext = ref<boolean>(true) //
const enableWebSearch = ref<boolean>(false) //
const uploadFiles = ref<string[]>([]) // URL
// Stream
const receiveMessageFullText = ref('')
@ -468,6 +471,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
userMessage.content,
conversationInAbortController.value,
enableContext.value,
enableWebSearch.value,
async (res) => {
const { code, data, msg } = JSON.parse(res.data)
if (code !== 0) {