mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 13:19:33 +08:00
refactor: network search module, support quick menu (#5291)
* refactor: Reconstruct the network search module to support quick menu switching between different suppliers. * refactor(GeminiProvider): simplify web search enablement logic * refactor(settings): remove unnecessary SettingDivider for cleaner layout * refactor(SelectModelButton): remove unused setModel function for improved performance * refactor(ApiService): simplify web search condition by removing redundant check
This commit is contained in:
parent
7be6ddfb59
commit
0a7bf99f9c
@ -2393,7 +2393,7 @@ export function isGenerateImageModel(model: Model): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenAIWebSearchParams(assistant: Assistant, model: Model): Record<string, any> {
|
export function getOpenAIWebSearchParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||||
if (WebSearchService.isWebSearchEnabled() && WebSearchService.isOverwriteEnabled()) {
|
if (WebSearchService.isWebSearchEnabled()) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
if (isWebSearchModel(model)) {
|
if (isWebSearchModel(model)) {
|
||||||
|
|||||||
@ -136,7 +136,7 @@
|
|||||||
"input.translate": "Translate to {{target_language}}",
|
"input.translate": "Translate to {{target_language}}",
|
||||||
"input.upload": "Upload image or document file",
|
"input.upload": "Upload image or document file",
|
||||||
"input.upload.document": "Upload document file (model does not support images)",
|
"input.upload.document": "Upload document file (model does not support images)",
|
||||||
"input.web_search": "Enable web search",
|
"input.web_search": "Web search",
|
||||||
"input.web_search.button.ok": "Go to Settings",
|
"input.web_search.button.ok": "Go to Settings",
|
||||||
"input.web_search.enable": "Enable web search",
|
"input.web_search.enable": "Enable web search",
|
||||||
"input.web_search.enable_content": "Need to check web search connectivity in settings first",
|
"input.web_search.enable_content": "Need to check web search connectivity in settings first",
|
||||||
@ -247,7 +247,10 @@
|
|||||||
"topics.export.title_naming_success": "Title generated successfully",
|
"topics.export.title_naming_success": "Title generated successfully",
|
||||||
"topics.export.title_naming_failed": "Failed to generate title, using default title",
|
"topics.export.title_naming_failed": "Failed to generate title, using default title",
|
||||||
"input.translating": "Translating...",
|
"input.translating": "Translating...",
|
||||||
"input.upload.upload_from_local": "Upload local file..."
|
"input.upload.upload_from_local": "Upload local file...",
|
||||||
|
"input.web_search.builtin": "Model Built-in",
|
||||||
|
"input.web_search.builtin.enabled_content": "Use the built-in web search function of the model",
|
||||||
|
"input.web_search.builtin.disabled_content": "The current model does not support web search"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
|
|||||||
@ -136,7 +136,7 @@
|
|||||||
"input.translate": "{{target_language}}に翻訳",
|
"input.translate": "{{target_language}}に翻訳",
|
||||||
"input.upload": "画像またはドキュメントをアップロード",
|
"input.upload": "画像またはドキュメントをアップロード",
|
||||||
"input.upload.document": "ドキュメントをアップロード(モデルは画像をサポートしません)",
|
"input.upload.document": "ドキュメントをアップロード(モデルは画像をサポートしません)",
|
||||||
"input.web_search": "ウェブ検索を有効にする",
|
"input.web_search": "ウェブ検索",
|
||||||
"input.web_search.button.ok": "設定に移動",
|
"input.web_search.button.ok": "設定に移動",
|
||||||
"input.web_search.enable": "ウェブ検索を有効にする",
|
"input.web_search.enable": "ウェブ検索を有効にする",
|
||||||
"input.web_search.enable_content": "ウェブ検索の接続性を先に設定で確認する必要があります",
|
"input.web_search.enable_content": "ウェブ検索の接続性を先に設定で確認する必要があります",
|
||||||
@ -247,7 +247,10 @@
|
|||||||
"topics.export.title_naming_success": "タイトルの生成に成功しました",
|
"topics.export.title_naming_success": "タイトルの生成に成功しました",
|
||||||
"topics.export.title_naming_failed": "タイトルの生成に失敗しました。デフォルトのタイトルを使用します",
|
"topics.export.title_naming_failed": "タイトルの生成に失敗しました。デフォルトのタイトルを使用します",
|
||||||
"input.translating": "翻訳中...",
|
"input.translating": "翻訳中...",
|
||||||
"input.upload.upload_from_local": "ローカルファイルをアップロード..."
|
"input.upload.upload_from_local": "ローカルファイルをアップロード...",
|
||||||
|
"input.web_search.builtin": "モデル内蔵",
|
||||||
|
"input.web_search.builtin.enabled_content": "モデル内蔵のウェブ検索機能を使用",
|
||||||
|
"input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
"collapse": "折りたたむ",
|
"collapse": "折りたたむ",
|
||||||
|
|||||||
@ -136,7 +136,7 @@
|
|||||||
"input.translate": "Перевести на {{target_language}}",
|
"input.translate": "Перевести на {{target_language}}",
|
||||||
"input.upload": "Загрузить изображение или документ",
|
"input.upload": "Загрузить изображение или документ",
|
||||||
"input.upload.document": "Загрузить документ (модель не поддерживает изображения)",
|
"input.upload.document": "Загрузить документ (модель не поддерживает изображения)",
|
||||||
"input.web_search": "Включить веб-поиск",
|
"input.web_search": "Веб-поиск",
|
||||||
"input.web_search.button.ok": "Перейти в Настройки",
|
"input.web_search.button.ok": "Перейти в Настройки",
|
||||||
"input.web_search.enable": "Включить веб-поиск",
|
"input.web_search.enable": "Включить веб-поиск",
|
||||||
"input.web_search.enable_content": "Необходимо предварительно проверить подключение к веб-поиску в настройках",
|
"input.web_search.enable_content": "Необходимо предварительно проверить подключение к веб-поиску в настройках",
|
||||||
@ -247,7 +247,10 @@
|
|||||||
"topics.export.title_naming_success": "Заголовок успешно создан",
|
"topics.export.title_naming_success": "Заголовок успешно создан",
|
||||||
"topics.export.title_naming_failed": "Не удалось создать заголовок, используется заголовок по умолчанию",
|
"topics.export.title_naming_failed": "Не удалось создать заголовок, используется заголовок по умолчанию",
|
||||||
"input.translating": "Перевод...",
|
"input.translating": "Перевод...",
|
||||||
"input.upload.upload_from_local": "Загрузить локальный файл..."
|
"input.upload.upload_from_local": "Загрузить локальный файл...",
|
||||||
|
"input.web_search.builtin": "Модель встроена",
|
||||||
|
"input.web_search.builtin.enabled_content": "Используйте встроенную функцию веб-поиска модели",
|
||||||
|
"input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
"collapse": "Свернуть",
|
"collapse": "Свернуть",
|
||||||
|
|||||||
@ -138,10 +138,13 @@
|
|||||||
"input.upload": "上传图片或文档",
|
"input.upload": "上传图片或文档",
|
||||||
"input.upload.upload_from_local": "上传本地文件...",
|
"input.upload.upload_from_local": "上传本地文件...",
|
||||||
"input.upload.document": "上传文档(模型不支持图片)",
|
"input.upload.document": "上传文档(模型不支持图片)",
|
||||||
"input.web_search": "开启网络搜索",
|
"input.web_search": "网络搜索",
|
||||||
"input.web_search.button.ok": "去设置",
|
"input.web_search.button.ok": "去设置",
|
||||||
"input.web_search.enable": "开启网络搜索",
|
"input.web_search.enable": "开启网络搜索",
|
||||||
"input.web_search.enable_content": "需要先在设置中检查网络搜索连通性",
|
"input.web_search.enable_content": "需要先在设置中检查网络搜索连通性",
|
||||||
|
"input.web_search.builtin": "模型内置",
|
||||||
|
"input.web_search.builtin.enabled_content": "使用模型内置的网络搜索功能",
|
||||||
|
"input.web_search.builtin.disabled_content": "当前模型不支持网络搜索功能",
|
||||||
"message.new.branch": "分支",
|
"message.new.branch": "分支",
|
||||||
"message.new.branch.created": "新分支已创建",
|
"message.new.branch.created": "新分支已创建",
|
||||||
"message.new.context": "清除上下文",
|
"message.new.context": "清除上下文",
|
||||||
|
|||||||
@ -136,7 +136,7 @@
|
|||||||
"input.translate": "翻譯成{{target_language}}",
|
"input.translate": "翻譯成{{target_language}}",
|
||||||
"input.upload": "上傳圖片或文件",
|
"input.upload": "上傳圖片或文件",
|
||||||
"input.upload.document": "上傳文件(模型不支援圖片)",
|
"input.upload.document": "上傳文件(模型不支援圖片)",
|
||||||
"input.web_search": "開啟網路搜尋",
|
"input.web_search": "網路搜尋",
|
||||||
"input.web_search.button.ok": "去設定",
|
"input.web_search.button.ok": "去設定",
|
||||||
"input.web_search.enable": "開啟網路搜尋",
|
"input.web_search.enable": "開啟網路搜尋",
|
||||||
"input.web_search.enable_content": "需要先在設定中開啟網路搜尋",
|
"input.web_search.enable_content": "需要先在設定中開啟網路搜尋",
|
||||||
@ -247,7 +247,10 @@
|
|||||||
"topics.export.title_naming_success": "標題生成成功",
|
"topics.export.title_naming_success": "標題生成成功",
|
||||||
"topics.export.title_naming_failed": "標題生成失敗,使用預設標題",
|
"topics.export.title_naming_failed": "標題生成失敗,使用預設標題",
|
||||||
"input.translating": "翻譯中...",
|
"input.translating": "翻譯中...",
|
||||||
"input.upload.upload_from_local": "上傳本地文件..."
|
"input.upload.upload_from_local": "上傳本地文件...",
|
||||||
|
"input.web_search.builtin": "模型內置",
|
||||||
|
"input.web_search.builtin.enabled_content": "使用模型內置的網路搜尋功能",
|
||||||
|
"input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
"collapse": "折疊",
|
"collapse": "折疊",
|
||||||
|
|||||||
@ -51,7 +51,6 @@ import {
|
|||||||
// import { CompletionUsage } from 'openai/resources'
|
// import { CompletionUsage } from 'openai/resources'
|
||||||
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import NarrowLayout from '../Messages/NarrowLayout'
|
import NarrowLayout from '../Messages/NarrowLayout'
|
||||||
@ -67,6 +66,7 @@ import NewContextButton from './NewContextButton'
|
|||||||
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
|
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
import TokenCount from './TokenCount'
|
import TokenCount from './TokenCount'
|
||||||
|
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@ -116,7 +116,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const currentMessageId = useRef<string>('')
|
const currentMessageId = useRef<string>('')
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
const navigate = useNavigate()
|
|
||||||
const { activedMcpServers } = useMCPServers()
|
const { activedMcpServers } = useMCPServers()
|
||||||
const { bases: knowledgeBases } = useKnowledgeBases()
|
const { bases: knowledgeBases } = useKnowledgeBases()
|
||||||
|
|
||||||
@ -131,6 +130,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
const knowledgeBaseButtonRef = useRef<KnowledgeBaseButtonRef>(null)
|
const knowledgeBaseButtonRef = useRef<KnowledgeBaseButtonRef>(null)
|
||||||
const mcpToolsButtonRef = useRef<MCPToolsButtonRef>(null)
|
const mcpToolsButtonRef = useRef<MCPToolsButtonRef>(null)
|
||||||
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
|
||||||
|
const webSearchButtonRef = useRef<WebSearchButtonRef>(null)
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const debouncedEstimate = useCallback(
|
const debouncedEstimate = useCallback(
|
||||||
@ -379,6 +379,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
mcpToolsButtonRef.current?.openResourcesList()
|
mcpToolsButtonRef.current?.openResourcesList()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('chat.input.web_search'),
|
||||||
|
description: '',
|
||||||
|
icon: <Globe />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
webSearchButtonRef.current?.openQuickPanel()
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||||
description: '',
|
description: '',
|
||||||
@ -770,46 +779,17 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
setSelectedKnowledgeBases(newKnowledgeBases ?? [])
|
setSelectedKnowledgeBases(newKnowledgeBases ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const showWebSearchEnableModal = () => {
|
|
||||||
window.modal.confirm({
|
|
||||||
title: t('chat.input.web_search.enable'),
|
|
||||||
content: t('chat.input.web_search.enable_content'),
|
|
||||||
centered: true,
|
|
||||||
okText: t('chat.input.web_search.button.ok'),
|
|
||||||
onOk: () => {
|
|
||||||
navigate('/settings/web-search')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldShowEnableModal = () => {
|
|
||||||
// 网络搜索功能是否未启用
|
|
||||||
const webSearchNotEnabled = !WebSearchService.isWebSearchEnabled()
|
|
||||||
// 非网络搜索模型:仅当网络搜索功能未启用时显示启用提示
|
|
||||||
if (!isWebSearchModel(model)) {
|
|
||||||
return webSearchNotEnabled
|
|
||||||
}
|
|
||||||
// 网络搜索模型:当允许覆盖但网络搜索功能未启用时显示启用提示
|
|
||||||
return WebSearchService.isOverwriteEnabled() && webSearchNotEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEnableWebSearch = () => {
|
|
||||||
if (shouldShowEnableModal()) {
|
|
||||||
showWebSearchEnableModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEnableGenerateImage = () => {
|
const onEnableGenerateImage = () => {
|
||||||
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
|
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWebSearchModel(model) && !WebSearchService.isWebSearchEnabled() && assistant.enableWebSearch) {
|
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
|
||||||
updateAssistant({ ...assistant, enableWebSearch: false })
|
updateAssistant({ ...assistant, enableWebSearch: false })
|
||||||
}
|
}
|
||||||
|
if (assistant.webSearchProviderId && !WebSearchService.isWebSearchEnabled(assistant.webSearchProviderId)) {
|
||||||
|
updateAssistant({ ...assistant, webSearchProviderId: undefined })
|
||||||
|
}
|
||||||
if (!isGenerateImageModel(model) && assistant.enableGenerateImage) {
|
if (!isGenerateImageModel(model) && assistant.enableGenerateImage) {
|
||||||
updateAssistant({ ...assistant, enableGenerateImage: false })
|
updateAssistant({ ...assistant, enableGenerateImage: false })
|
||||||
}
|
}
|
||||||
@ -938,14 +918,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
/>
|
/>
|
||||||
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||||
<ToolbarButton type="text" onClick={onEnableWebSearch}>
|
|
||||||
<Globe
|
|
||||||
size={18}
|
|
||||||
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
|
|
||||||
/>
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
{showKnowledgeIcon && (
|
{showKnowledgeIcon && (
|
||||||
<KnowledgeBaseButton
|
<KnowledgeBaseButton
|
||||||
ref={knowledgeBaseButtonRef}
|
ref={knowledgeBaseButtonRef}
|
||||||
|
|||||||
129
src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx
Normal file
129
src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
|
import { isWebSearchModel } from '@renderer/config/models'
|
||||||
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
|
||||||
|
import WebSearchService from '@renderer/services/WebSearchService'
|
||||||
|
import { Assistant, WebSearchProvider } from '@renderer/types'
|
||||||
|
import { hasObjectKey } from '@renderer/utils'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { Globe, Settings } from 'lucide-react'
|
||||||
|
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export interface WebSearchButtonRef {
|
||||||
|
openQuickPanel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ref?: React.RefObject<WebSearchButtonRef | null>
|
||||||
|
assistant: Assistant
|
||||||
|
ToolbarButton: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const quickPanel = useQuickPanel()
|
||||||
|
const { providers } = useWebSearchProviders()
|
||||||
|
const { updateAssistant } = useAssistant(assistant.id)
|
||||||
|
|
||||||
|
const updateSelectedWebSearchProvider = useCallback(
|
||||||
|
(providerId: WebSearchProvider['id']) => {
|
||||||
|
// TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentWebSearchProviderId = assistant.webSearchProviderId
|
||||||
|
const newWebSearchProviderId = currentWebSearchProviderId === providerId ? undefined : providerId
|
||||||
|
updateAssistant({ ...assistant, webSearchProviderId: newWebSearchProviderId, enableWebSearch: false })
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
[assistant, updateAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateSelectedWebSearchBuiltin = useCallback(() => {
|
||||||
|
// TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿
|
||||||
|
setTimeout(() => {
|
||||||
|
updateAssistant({ ...assistant, webSearchProviderId: undefined, enableWebSearch: !assistant.enableWebSearch })
|
||||||
|
}, 200)
|
||||||
|
}, [assistant, updateAssistant])
|
||||||
|
|
||||||
|
const providerItems = useMemo<QuickPanelListItem[]>(() => {
|
||||||
|
const isWebSearchModelEnabled = assistant.model && isWebSearchModel(assistant.model)
|
||||||
|
|
||||||
|
const items: QuickPanelListItem[] = providers.map((p) => ({
|
||||||
|
label: p.name,
|
||||||
|
description: WebSearchService.isWebSearchEnabled(p.id)
|
||||||
|
? hasObjectKey(p, 'apiKey')
|
||||||
|
? t('settings.websearch.apikey')
|
||||||
|
: t('settings.websearch.free')
|
||||||
|
: t('chat.input.web_search.enable_content'),
|
||||||
|
icon: <Globe />,
|
||||||
|
isSelected: p.id === assistant?.webSearchProviderId,
|
||||||
|
disabled: !WebSearchService.isWebSearchEnabled(p.id),
|
||||||
|
action: () => updateSelectedWebSearchProvider(p.id)
|
||||||
|
}))
|
||||||
|
|
||||||
|
items.unshift({
|
||||||
|
label: t('chat.input.web_search.builtin'),
|
||||||
|
description: isWebSearchModelEnabled
|
||||||
|
? t('chat.input.web_search.builtin.enabled_content')
|
||||||
|
: t('chat.input.web_search.builtin.disabled_content'),
|
||||||
|
icon: <Globe />,
|
||||||
|
isSelected: assistant.enableWebSearch,
|
||||||
|
disabled: !isWebSearchModelEnabled,
|
||||||
|
action: () => updateSelectedWebSearchBuiltin()
|
||||||
|
})
|
||||||
|
items.push({
|
||||||
|
label: '前往设置' + '...',
|
||||||
|
icon: <Settings />,
|
||||||
|
action: () => navigate('/settings/web-search')
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}, [
|
||||||
|
assistant.model,
|
||||||
|
assistant.enableWebSearch,
|
||||||
|
assistant.webSearchProviderId,
|
||||||
|
providers,
|
||||||
|
t,
|
||||||
|
updateSelectedWebSearchProvider,
|
||||||
|
updateSelectedWebSearchBuiltin,
|
||||||
|
navigate
|
||||||
|
])
|
||||||
|
|
||||||
|
const openQuickPanel = useCallback(() => {
|
||||||
|
quickPanel.open({
|
||||||
|
title: t('chat.input.web_search'),
|
||||||
|
list: providerItems,
|
||||||
|
symbol: '?'
|
||||||
|
})
|
||||||
|
}, [quickPanel, providerItems, t])
|
||||||
|
|
||||||
|
const handleOpenQuickPanel = useCallback(() => {
|
||||||
|
if (quickPanel.isVisible && quickPanel.symbol === '?') {
|
||||||
|
quickPanel.close()
|
||||||
|
} else {
|
||||||
|
openQuickPanel()
|
||||||
|
}
|
||||||
|
}, [openQuickPanel, quickPanel])
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
openQuickPanel
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
|
||||||
|
<Globe
|
||||||
|
size={18}
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
assistant?.webSearchProviderId || assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebSearchButton
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
|
import { isWebSearchModel } from '@renderer/config/models'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { getProviderName } from '@renderer/services/ProviderService'
|
import { getProviderName } from '@renderer/services/ProviderService'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
@ -14,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SelectModelButton: FC<Props> = ({ assistant }) => {
|
const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||||
const { model, setModel } = useAssistant(assistant.id)
|
const { model, updateAssistant } = useAssistant(assistant.id)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (isLocalAi) {
|
if (isLocalAi) {
|
||||||
@ -25,7 +26,15 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
|||||||
event.currentTarget.blur()
|
event.currentTarget.blur()
|
||||||
const selectedModel = await SelectModelPopup.show({ model })
|
const selectedModel = await SelectModelPopup.show({ model })
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
setModel(selectedModel)
|
// 避免更新数据造成关闭弹框的卡顿
|
||||||
|
setTimeout(() => {
|
||||||
|
const enabledWebSearch = isWebSearchModel(selectedModel)
|
||||||
|
updateAssistant({
|
||||||
|
...assistant,
|
||||||
|
model: selectedModel,
|
||||||
|
enableWebSearch: enabledWebSearch && assistant.enableWebSearch
|
||||||
|
})
|
||||||
|
}, 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setContentLimit, setMaxResult, setOverwrite, setSearchWithTime } from '@renderer/store/websearch'
|
import { setContentLimit, setMaxResult, setSearchWithTime } from '@renderer/store/websearch'
|
||||||
import { Input, Slider, Switch, Tooltip } from 'antd'
|
import { Input, Slider, Switch, Tooltip } from 'antd'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { Info } from 'lucide-react'
|
import { Info } from 'lucide-react'
|
||||||
@ -11,7 +11,6 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
|
|||||||
const BasicSettings: FC = () => {
|
const BasicSettings: FC = () => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const searchWithTime = useAppSelector((state) => state.websearch.searchWithTime)
|
const searchWithTime = useAppSelector((state) => state.websearch.searchWithTime)
|
||||||
const overwrite = useAppSelector((state) => state.websearch.overwrite)
|
|
||||||
const maxResults = useAppSelector((state) => state.websearch.maxResults)
|
const maxResults = useAppSelector((state) => state.websearch.maxResults)
|
||||||
const contentLimit = useAppSelector((state) => state.websearch.contentLimit)
|
const contentLimit = useAppSelector((state) => state.websearch.contentLimit)
|
||||||
|
|
||||||
@ -26,16 +25,6 @@ const BasicSettings: FC = () => {
|
|||||||
<SettingRowTitle>{t('settings.websearch.search_with_time')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.websearch.search_with_time')}</SettingRowTitle>
|
||||||
<Switch checked={searchWithTime} onChange={(checked) => dispatch(setSearchWithTime(checked))} />
|
<Switch checked={searchWithTime} onChange={(checked) => dispatch(setSearchWithTime(checked))} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>
|
|
||||||
{t('settings.websearch.overwrite')}
|
|
||||||
<Tooltip title={t('settings.websearch.overwrite_tooltip')} placement="right">
|
|
||||||
<Info size={16} color="var(--color-icon)" style={{ marginLeft: 5, cursor: 'pointer' }} />
|
|
||||||
</Tooltip>
|
|
||||||
</SettingRowTitle>
|
|
||||||
<Switch checked={overwrite} onChange={(checked) => dispatch(setOverwrite(checked))} />
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider style={{ marginTop: 15, marginBottom: 10 }} />
|
<SettingDivider style={{ marginTop: 15, marginBottom: 10 }} />
|
||||||
<SettingRow style={{ height: 40 }}>
|
<SettingRow style={{ height: 40 }}>
|
||||||
<SettingRowTitle>{t('settings.websearch.search_max_result')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.websearch.search_max_result')}</SettingRowTitle>
|
||||||
|
|||||||
@ -185,7 +185,7 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
|
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
|
||||||
{t('settings.provider.api_host')}
|
{t('settings.provider.api_host')}
|
||||||
</SettingSubtitle>
|
</SettingSubtitle>
|
||||||
<Flex>
|
<Flex gap={8}>
|
||||||
<Input
|
<Input
|
||||||
value={apiHost}
|
value={apiHost}
|
||||||
placeholder={t('settings.provider.api_host')}
|
placeholder={t('settings.provider.api_host')}
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import {
|
|||||||
filterEmptyMessages,
|
filterEmptyMessages,
|
||||||
filterUserRoleStartMessages
|
filterUserRoleStartMessages
|
||||||
} from '@renderer/services/MessagesService'
|
} from '@renderer/services/MessagesService'
|
||||||
import WebSearchService from '@renderer/services/WebSearchService'
|
|
||||||
import {
|
import {
|
||||||
Assistant,
|
Assistant,
|
||||||
FileType,
|
FileType,
|
||||||
@ -285,7 +284,7 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
const tools: ToolListUnion = []
|
const tools: ToolListUnion = []
|
||||||
const toolResponses: MCPToolResponse[] = []
|
const toolResponses: MCPToolResponse[] = []
|
||||||
|
|
||||||
if (!WebSearchService.isOverwriteEnabled() && assistant.enableWebSearch && isWebSearchModel(model)) {
|
if (assistant.enableWebSearch && isWebSearchModel(model)) {
|
||||||
tools.push({
|
tools.push({
|
||||||
// @ts-ignore googleSearch is not a valid tool for Gemini
|
// @ts-ignore googleSearch is not a valid tool for Gemini
|
||||||
googleSearch: {}
|
googleSearch: {}
|
||||||
|
|||||||
@ -338,6 +338,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
const defaultModel = getDefaultModel()
|
const defaultModel = getDefaultModel()
|
||||||
const model = assistant.model || defaultModel
|
const model = assistant.model || defaultModel
|
||||||
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
|
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
|
||||||
|
const isEnabledWebSearch = assistant.enableWebSearch || !!assistant.webSearchProviderId
|
||||||
messages = addImageFileToContents(messages)
|
messages = addImageFileToContents(messages)
|
||||||
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
let systemMessage = { role: 'system', content: assistant.prompt || '' }
|
||||||
if (isOpenAIoSeries(model) && !OPENAI_NO_SUPPORT_DEV_ROLE_MODELS.includes(model.id)) {
|
if (isOpenAIoSeries(model) && !OPENAI_NO_SUPPORT_DEV_ROLE_MODELS.includes(model.id)) {
|
||||||
@ -636,7 +637,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
} as LLMWebSearchCompleteChunk)
|
} as LLMWebSearchCompleteChunk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (assistant.enableWebSearch && isZhipuModel(model) && finishReason === 'stop' && chunk?.web_search) {
|
if (isEnabledWebSearch && isZhipuModel(model) && finishReason === 'stop' && chunk?.web_search) {
|
||||||
onChunk({
|
onChunk({
|
||||||
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
|
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
|
||||||
llm_web_search: {
|
llm_web_search: {
|
||||||
@ -645,7 +646,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
} as LLMWebSearchCompleteChunk)
|
} as LLMWebSearchCompleteChunk)
|
||||||
}
|
}
|
||||||
if (assistant.enableWebSearch && isHunyuanSearchModel(model) && chunk?.search_info?.search_results) {
|
if (isEnabledWebSearch && isHunyuanSearchModel(model) && chunk?.search_info?.search_results) {
|
||||||
onChunk({
|
onChunk({
|
||||||
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
|
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
|
||||||
llm_web_search: {
|
llm_web_search: {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { getOpenAIWebSearchParams, isOpenAIWebSearch, isWebSearchModel } from '@renderer/config/models'
|
import { getOpenAIWebSearchParams, isOpenAIWebSearch } from '@renderer/config/models'
|
||||||
import { SEARCH_SUMMARY_PROMPT } from '@renderer/config/prompts'
|
import { SEARCH_SUMMARY_PROMPT } from '@renderer/config/prompts'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import {
|
import {
|
||||||
@ -42,7 +42,7 @@ async function fetchExternalTool(
|
|||||||
// 可能会有重复?
|
// 可能会有重复?
|
||||||
const knowledgeBaseIds = getKnowledgeBaseIds(lastUserMessage)
|
const knowledgeBaseIds = getKnowledgeBaseIds(lastUserMessage)
|
||||||
const hasKnowledgeBase = !isEmpty(knowledgeBaseIds)
|
const hasKnowledgeBase = !isEmpty(knowledgeBaseIds)
|
||||||
const webSearchProvider = WebSearchService.getWebSearchProvider()
|
const webSearchProvider = WebSearchService.getWebSearchProvider(assistant.webSearchProviderId)
|
||||||
|
|
||||||
// --- Keyword/Question Extraction Function ---
|
// --- Keyword/Question Extraction Function ---
|
||||||
const extract = async (): Promise<ExtractResults | undefined> => {
|
const extract = async (): Promise<ExtractResults | undefined> => {
|
||||||
@ -120,7 +120,7 @@ async function fetchExternalTool(
|
|||||||
// Use the consolidated processWebsearch function
|
// Use the consolidated processWebsearch function
|
||||||
WebSearchService.createAbortSignal(lastUserMessage.id)
|
WebSearchService.createAbortSignal(lastUserMessage.id)
|
||||||
return {
|
return {
|
||||||
results: await WebSearchService.processWebsearch(webSearchProvider, extractResults),
|
results: await WebSearchService.processWebsearch(webSearchProvider!, extractResults),
|
||||||
source: WebSearchSource.WEBSEARCH
|
source: WebSearchSource.WEBSEARCH
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -157,8 +157,7 @@ async function fetchExternalTool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldWebSearch =
|
const shouldWebSearch = !!assistant.webSearchProviderId
|
||||||
assistant.enableWebSearch && (!isWebSearchModel(assistant.model!) || WebSearchService.isOverwriteEnabled())
|
|
||||||
|
|
||||||
// --- Execute Extraction and Searches ---
|
// --- Execute Extraction and Searches ---
|
||||||
const extractResults = await extract()
|
const extractResults = await extract()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import WebSearchEngineProvider from '@renderer/providers/WebSearchProvider'
|
import WebSearchEngineProvider from '@renderer/providers/WebSearchProvider'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setDefaultProvider, WebSearchState } from '@renderer/store/websearch'
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||||
import { hasObjectKey } from '@renderer/utils'
|
import { hasObjectKey } from '@renderer/utils'
|
||||||
import { addAbortController } from '@renderer/utils/abortController'
|
import { addAbortController } from '@renderer/utils/abortController'
|
||||||
@ -43,9 +43,9 @@ class WebSearchService {
|
|||||||
* @public
|
* @public
|
||||||
* @returns 如果默认搜索提供商已启用则返回true,否则返回false
|
* @returns 如果默认搜索提供商已启用则返回true,否则返回false
|
||||||
*/
|
*/
|
||||||
public isWebSearchEnabled(): boolean {
|
public isWebSearchEnabled(providerId?: WebSearchProvider['id']): boolean {
|
||||||
const { defaultProvider, providers } = this.getWebSearchState()
|
const { providers } = this.getWebSearchState()
|
||||||
const provider = providers.find((provider) => provider.id === defaultProvider)
|
const provider = providers.find((provider) => provider.id === providerId)
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return false
|
return false
|
||||||
@ -67,6 +67,8 @@ class WebSearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated 支持在快捷菜单中自选搜索供应商,所以这个不再适用
|
||||||
|
*
|
||||||
* 检查是否启用覆盖搜索
|
* 检查是否启用覆盖搜索
|
||||||
* @public
|
* @public
|
||||||
* @returns 如果启用覆盖搜索则返回true,否则返回false
|
* @returns 如果启用覆盖搜索则返回true,否则返回false
|
||||||
@ -80,21 +82,10 @@ class WebSearchService {
|
|||||||
* 获取当前默认的网络搜索提供商
|
* 获取当前默认的网络搜索提供商
|
||||||
* @public
|
* @public
|
||||||
* @returns 网络搜索提供商
|
* @returns 网络搜索提供商
|
||||||
* @throws 如果找不到默认提供商则抛出错误
|
|
||||||
*/
|
*/
|
||||||
public getWebSearchProvider(): WebSearchProvider {
|
public getWebSearchProvider(providerId?: string): WebSearchProvider | undefined {
|
||||||
const { defaultProvider, providers } = this.getWebSearchState()
|
const { providers } = this.getWebSearchState()
|
||||||
let provider = providers.find((provider) => provider.id === defaultProvider)
|
const provider = providers.find((provider) => provider.id === providerId)
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
provider = providers[0]
|
|
||||||
if (provider) {
|
|
||||||
// 可选:自动更新默认提供商
|
|
||||||
store.dispatch(setDefaultProvider(provider.id))
|
|
||||||
} else {
|
|
||||||
throw new Error(`No web search providers available`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface SubscribeSource {
|
|||||||
|
|
||||||
export interface WebSearchState {
|
export interface WebSearchState {
|
||||||
// 默认搜索提供商的ID
|
// 默认搜索提供商的ID
|
||||||
|
/** @deprecated 支持在快捷菜单中自选搜索供应商,所以这个不再适用 */
|
||||||
defaultProvider: string
|
defaultProvider: string
|
||||||
// 所有可用的搜索提供商列表
|
// 所有可用的搜索提供商列表
|
||||||
providers: WebSearchProvider[]
|
providers: WebSearchProvider[]
|
||||||
@ -21,6 +22,7 @@ export interface WebSearchState {
|
|||||||
// 订阅源列表
|
// 订阅源列表
|
||||||
subscribeSources: SubscribeSource[]
|
subscribeSources: SubscribeSource[]
|
||||||
// 是否覆盖服务商搜索
|
// 是否覆盖服务商搜索
|
||||||
|
/** @deprecated 支持在快捷菜单中自选搜索供应商,所以这个不再适用 */
|
||||||
overwrite: boolean
|
overwrite: boolean
|
||||||
contentLimit?: number
|
contentLimit?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,9 @@ export type Assistant = {
|
|||||||
defaultModel?: Model
|
defaultModel?: Model
|
||||||
settings?: Partial<AssistantSettings>
|
settings?: Partial<AssistantSettings>
|
||||||
messages?: AssistantMessage[]
|
messages?: AssistantMessage[]
|
||||||
|
/** enableWebSearch 代表使用模型内置网络搜索功能 */
|
||||||
enableWebSearch?: boolean
|
enableWebSearch?: boolean
|
||||||
|
webSearchProviderId?: WebSearchProvider['id']
|
||||||
enableGenerateImage?: boolean
|
enableGenerateImage?: boolean
|
||||||
mcpServers?: MCPServer[]
|
mcpServers?: MCPServer[]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user