From 941f86008bc6e1c3971f0887eb66aa8de533959f Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:37:46 +0800 Subject: [PATCH] fix: restrict using gemini native tools and mcp tools simultaneously (#9361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(providers): 添加对支持URL上下文的提供者类型的检查 新增 `isSupportUrlContextProvider` 函数用于检查提供者是否支持URL上下文功能 * fix(InputbarTools): 修复URL上下文按钮显示条件判断 添加对模型提供者是否支持URL上下文的检查 * fix(gemini): 修复原生工具与函数调用同时启用时的冲突 当同时启用web搜索和URL上下文工具时,如果已存在函数调用工具,则添加警告日志提示当前不支持同时使用 * feat(i18n): 限制 Gemini 同时使用网页上下文与 MCP 工具 添加多语言翻译文案和功能实现,当用户尝试同时启用网页上下文和 MCP 工具时,显示警告提示并自动禁用网页上下文 * perf(WebSearchButton): 使用定时器优化更新性能避免卡顿 移除startTransition并使用useTimer的setTimeoutTimer来延迟更新操作,解决updateAssistant导致的快捷面板关闭卡顿问题 * feat(i18n): 限制 Gemini 原生搜索工具与函数调用的同时使用 添加对 Gemini 原生搜索工具与函数调用同时使用时的冲突检测 更新相关国际化文案和功能实现 * fix(GeminiAPIClient): 修复工具使用模式判断逻辑 当工具使用模式为'prompt'时应该允许使用native tool * reafactor: 简化 Gemini 模型下工具使用模式的 URL 上下文和网页搜索检查逻辑 * fix(WebSearchButton): 修复条件判断 * refactor(utils): 提取函数工具使用模式判断逻辑到单独函数 * test(assistant): 添加工具使用模式功能的单元测试 * refactor(InputbarTools): 使用isGeminiModel函数替代字符串检查 简化模型类型检查逻辑,提高代码可读性和维护性 * perf(Inputbar): 使用setTimeoutTimer替代startTransition解决性能问题 --- .../aiCore/clients/gemini/GeminiAPIClient.ts | 23 +++++---- src/renderer/src/config/models.ts | 5 ++ src/renderer/src/config/providers.ts | 14 ++++++ src/renderer/src/i18n/locales/en-us.json | 4 +- src/renderer/src/i18n/locales/ja-jp.json | 4 +- src/renderer/src/i18n/locales/ru-ru.json | 4 +- src/renderer/src/i18n/locales/zh-cn.json | 4 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- src/renderer/src/i18n/translate/el-gr.json | 4 +- src/renderer/src/i18n/translate/es-es.json | 4 +- src/renderer/src/i18n/translate/fr-fr.json | 4 +- src/renderer/src/i18n/translate/pt-pt.json | 4 +- .../src/pages/home/Inputbar/InputbarTools.tsx | 6 ++- .../pages/home/Inputbar/MCPToolsButton.tsx | 31 ++++++++++-- .../pages/home/Inputbar/UrlContextbutton.tsx | 17 ++++++- .../pages/home/Inputbar/WebSearchButton.tsx | 50 ++++++++++++++----- .../src/utils/__tests__/assistant.test.ts | 40 +++++++++++++++ src/renderer/src/utils/assistant.ts | 5 ++ src/renderer/src/utils/mcp-tools.ts | 5 +- 19 files changed, 192 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/utils/__tests__/assistant.test.ts create mode 100644 src/renderer/src/utils/assistant.ts diff --git a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts index bdd7689d6f..b6e17398bf 100644 --- a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts +++ b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts @@ -52,6 +52,7 @@ import { GeminiSdkRawOutput, GeminiSdkToolCall } from '@renderer/types/sdk' +import { isToolUseModeFunction } from '@renderer/utils/assistant' import { geminiFunctionCallToMcpTool, isEnabledToolUse, @@ -476,16 +477,20 @@ export class GeminiAPIClient extends BaseApiClient< } } - if (enableWebSearch) { - tools.push({ - googleSearch: {} - }) - } + if (tools.length === 0 || !isToolUseModeFunction(assistant)) { + if (enableWebSearch) { + tools.push({ + googleSearch: {} + }) + } - if (enableUrlContext) { - tools.push({ - urlContext: {} - }) + if (enableUrlContext) { + tools.push({ + urlContext: {} + }) + } + } else if (enableWebSearch || enableUrlContext) { + logger.warn('Native tools cannot be used with function calling for now.') } if (isGemmaModel(model) && assistant.prompt) { diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 19247e63d3..fb40d070d9 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -3307,6 +3307,11 @@ export const isGPT5SeriesModel = (model: Model) => { return modelId.includes('gpt-5') } +export const isGeminiModel = (model: Model) => { + const modelId = getLowerBaseModelName(model.id) + return modelId.includes('gemini') +} + export const isOpenAIOpenWeightModel = (model: Model) => { const modelId = getLowerBaseModelName(model.id) return modelId.includes('gpt-oss') diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index ac73f47cc7..795b243c62 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -56,6 +56,7 @@ import { isSystemProvider, OpenAIServiceTiers, Provider, + ProviderType, SystemProvider, SystemProviderId } from '@renderer/types' @@ -1303,3 +1304,16 @@ export const isSupportServiceTierProvider = (provider: Provider) => { (isSystemProvider(provider) && !NOT_SUPPORT_SERVICE_TIER_PROVIDERS.some((pid) => pid === provider.id)) ) } + +const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[] + +export const isSupportUrlContextProvider = (provider: Provider) => { + return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type) +} + +const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[] + +/** 判断是否是使用 Gemini 原生搜索工具的 provider. 目前假设只有官方 API 使用原生工具 */ +export const isGeminiWebSearchProvider = (provider: Provider) => { + return SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS.some((id) => id === provider.id) +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 6b3093e52f..19240331b4 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -390,8 +390,10 @@ "parse_tool_call": "Unable to convert to a valid tool call format: {{toolCall}}" }, "warning": { + "gemini_web_search": "Gemini does not support using native web search tools and function calling simultaneously", "multiple_tools": "Multiple matching MCP tools exist, {{tool}} has been selected", - "no_tool": "No matching MCP tool found for {{tool}}" + "no_tool": "No matching MCP tool found for {{tool}}", + "url_context": "Gemini does not support using url context and function calling simultaneously" } }, "message": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 499ad0aa15..d56c0590b3 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -390,8 +390,10 @@ "parse_tool_call": "有効なツール呼び出し形式に変換できません:{{toolCall}}" }, "warning": { + "gemini_web_search": "Geminiは、ネイティブのネットワーク検索ツールと関数呼び出しを同時に使用することをサポートしていません。", "multiple_tools": "複数の一致するMCPツールが存在するため、{{tool}} が選択されました", - "no_tool": "必要なMCPツール {{tool}} が見つかりません" + "no_tool": "必要なMCPツール {{tool}} が見つかりません", + "url_context": "Geminiは、URLコンテキストと関数呼び出しを同時に使用することをサポートしていません。" } }, "message": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 23c2a3efd4..43922086c3 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -390,8 +390,10 @@ "parse_tool_call": "Не удалось преобразовать в действительный формат вызова инструмента: {{toolCall}}" }, "warning": { + "gemini_web_search": "Gemini не поддерживает одновременное использование встроенного инструмента поиска в сети и вызова функций", "multiple_tools": "Существует несколько совпадающих инструментов MCP, выбран {{tool}}", - "no_tool": "Не удалось сопоставить требуемый инструмент MCP {{tool}}" + "no_tool": "Не удалось сопоставить требуемый инструмент MCP {{tool}}", + "url_context": "Gemini не поддерживает одновременное использование контекста веб-страницы и вызова функций" } }, "message": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c500655869..d90ab78caa 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -390,8 +390,10 @@ "parse_tool_call": "无法转换为有效的工具调用格式:{{toolCall}}" }, "warning": { + "gemini_web_search": "Gemini 不支持同时使用原生网络搜索工具与函数调用", "multiple_tools": "存在多个匹配的MCP工具,已选择 {{tool}}", - "no_tool": "未匹配到所需的MCP工具 {{tool}}" + "no_tool": "未匹配到所需的MCP工具 {{tool}}", + "url_context": "Gemini 不支持同时使用网页上下文与函数调用" } }, "message": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 0ef8fdd50a..6bd3bcf2b9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -390,8 +390,10 @@ "parse_tool_call": "無法轉換為有效的工具呼叫格式:{{toolCall}}" }, "warning": { + "gemini_web_search": "Gemini 不支援同時使用原生網路搜尋工具與函數呼叫", "multiple_tools": "存在多個匹配的MCP工具,已選擇 {{tool}}", - "no_tool": "未匹配到所需的MCP工具 {{tool}}" + "no_tool": "未匹配到所需的MCP工具 {{tool}}", + "url_context": "Gemini 不支援同時使用網頁內容與函數呼叫" } }, "message": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index f9b54f88fc..c91c73d6e0 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -390,8 +390,10 @@ "parse_tool_call": "Δεν είναι δυνατή η μετατροπή σε έγκυρη μορφή κλήσης εργαλείου: {{toolCall}}" }, "warning": { + "gemini_web_search": "Το Gemini δεν υποστηρίζει την ταυτόχρονη χρήση του εργαλείου αυτόματης αναζήτησης και της κλήσης συναρτήσεων", "multiple_tools": "Υπάρχουν πολλαπλά εργαλεία MCP που ταιριάζουν, επιλέχθηκε το {{tool}}", - "no_tool": "Δεν βρέθηκε το απαιτούμενο εργαλείο MCP {{tool}}" + "no_tool": "Δεν βρέθηκε το απαιτούμενο εργαλείο MCP {{tool}}", + "url_context": "Το Gemini δεν υποστηρίζει την ταυτόχρονη χρήση πλοήγησης στον ιστό και κλήσης συναρτήσεων" } }, "message": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 9565489985..ec6c42f7cc 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -390,8 +390,10 @@ "parse_tool_call": "No se puede convertir al formato de llamada de herramienta válido: {{toolCall}}" }, "warning": { + "gemini_web_search": "Gemini no admite el uso simultáneo de herramientas de búsqueda nativa y llamadas de funciones", "multiple_tools": "Existen múltiples herramientas MCP coincidentes, se ha seleccionado {{tool}}", - "no_tool": "No se encontró la herramienta MCP requerida {{tool}}" + "no_tool": "No se encontró la herramienta MCP requerida {{tool}}", + "url_context": "Gemini no admite el uso simultáneo del contexto de la página web y las llamadas a funciones." } }, "message": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 87fd54daef..e3166691c9 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -390,8 +390,10 @@ "parse_tool_call": "Impossible de convertir au format d'appel d'outil valide : {{toolCall}}" }, "warning": { + "gemini_web_search": "Gemini ne prend pas en charge l'utilisation simultanée de l'outil de recherche natif et de l'appel de fonctions", "multiple_tools": "Il existe plusieurs outils MCP correspondants, {{tool}} a été sélectionné", - "no_tool": "Aucun outil MCP requis {{tool}} n'a été trouvé" + "no_tool": "Aucun outil MCP requis {{tool}} n'a été trouvé", + "url_context": "Gemini ne prend pas en charge l'utilisation simultanée du contexte de la page Web et des appels de fonction" } }, "message": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 999476167c..fd06442a63 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -390,8 +390,10 @@ "parse_tool_call": "Não é possível converter para um formato de chamada de ferramenta válido: {{toolCall}}" }, "warning": { + "gemini_web_search": "O Gemini não suporta o uso simultâneo da ferramenta de pesquisa nativa e da chamada de funções.", "multiple_tools": "Existem várias ferramentas MCP correspondentes, a ferramenta {{tool}} foi selecionada", - "no_tool": "Nenhuma ferramenta MCP necessária correspondente encontrada {{tool}}" + "no_tool": "Nenhuma ferramenta MCP necessária correspondente encontrada {{tool}}", + "url_context": "O Gemini não suporta o uso simultâneo de contexto da página da web e chamadas de função" } }, "message": { diff --git a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx index 0f60ef9e7f..079b7801b1 100644 --- a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx +++ b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx @@ -1,6 +1,8 @@ import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd' import { QuickPanelListItem } from '@renderer/components/QuickPanel' -import { isGenerateImageModel, isMandatoryWebSearchModel } from '@renderer/config/models' +import { isGeminiModel, isGenerateImageModel, isMandatoryWebSearchModel } from '@renderer/config/models' +import { isSupportUrlContextProvider } from '@renderer/config/providers' +import { getProviderByModel } from '@renderer/services/AssistantService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools' import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types' @@ -347,7 +349,7 @@ const InputbarTools = ({ key: 'url_context', label: t('chat.input.url_context'), component: , - condition: model.id.toLowerCase().includes('gemini') + condition: isGeminiModel(model) && isSupportUrlContextProvider(getProviderByModel(model)) }, { key: 'knowledge_base', diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index b4589fbc24..7d0b125b91 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -1,9 +1,13 @@ import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel' +import { isGeminiModel } from '@renderer/config/models' +import { isGeminiWebSearchProvider, isSupportUrlContextProvider } from '@renderer/config/providers' import { useAssistant } from '@renderer/hooks/useAssistant' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useTimer } from '@renderer/hooks/useTimer' +import { getProviderByModel } from '@renderer/services/AssistantService' import { EventEmitter } from '@renderer/services/EventService' import { Assistant, MCPPrompt, MCPResource, MCPServer } from '@renderer/types' +import { isToolUseModeFunction } from '@renderer/utils/assistant' import { Form, Input, Tooltip } from 'antd' import { CircleX, Hammer, Plus } from 'lucide-react' import React, { FC, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' @@ -117,6 +121,7 @@ const MCPToolsButton: FC = ({ ref, setInputValue, resizeTextArea, Toolbar const [form] = Form.useForm() const { updateAssistant, assistant } = useAssistant(props.assistant.id) + const model = assistant.model const { setTimeoutTimer } = useTimer() // 使用 useRef 存储不需要触发重渲染的值 @@ -135,13 +140,33 @@ const MCPToolsButton: FC = ({ ref, setInputValue, resizeTextArea, Toolbar ) const handleMcpServerSelect = useCallback( (server: MCPServer) => { + const update = { ...assistant } if (assistantMcpServers.some((s) => s.id === server.id)) { - updateAssistant({ ...assistant, mcpServers: mcpServers?.filter((s) => s.id !== server.id) }) + update.mcpServers = mcpServers.filter((s) => s.id !== server.id) } else { - updateAssistant({ ...assistant, mcpServers: [...mcpServers, server] }) + update.mcpServers = [...mcpServers, server] } + + // only for gemini + if (update.mcpServers.length > 0 && isGeminiModel(model) && isToolUseModeFunction(assistant)) { + const provider = getProviderByModel(model) + if (isSupportUrlContextProvider(provider) && assistant.enableUrlContext) { + window.message.warning(t('chat.mcp.warning.url_context')) + update.enableUrlContext = false + } + if ( + // 非官方 API (openrouter etc.) 可能支持同时启用内置搜索和函数调用 + // 这里先假设 gemini type 和 vertexai type 不支持 + isGeminiWebSearchProvider(provider) && + assistant.enableWebSearch + ) { + window.message.warning(t('chat.mcp.warning.gemini_web_search')) + update.enableWebSearch = false + } + } + updateAssistant(update) }, - [assistant, assistantMcpServers, mcpServers, updateAssistant] + [assistant, assistantMcpServers, mcpServers, model, t, updateAssistant] ) // 使用 useRef 缓存事件处理函数 diff --git a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx index cfbf12f538..ff92fe1161 100644 --- a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx +++ b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx @@ -1,6 +1,7 @@ import { useAssistant } from '@renderer/hooks/useAssistant' import { useTimer } from '@renderer/hooks/useTimer' import { Assistant } from '@renderer/types' +import { isToolUseModeFunction } from '@renderer/utils/assistant' import { Tooltip } from 'antd' import { Link } from 'lucide-react' import { FC, memo, useCallback } from 'react' @@ -27,11 +28,23 @@ const UrlContextButton: FC = ({ assistant, ToolbarButton }) => { setTimeoutTimer( 'handleToggle', () => { - updateAssistant({ ...assistant, enableUrlContext: urlContentNewState }) + const update = { ...assistant } + if ( + assistant.mcpServers && + assistant.mcpServers.length > 0 && + urlContentNewState === true && + isToolUseModeFunction(assistant) + ) { + update.enableUrlContext = false + window.message.warning(t('chat.mcp.warning.url_context')) + } else { + update.enableUrlContext = urlContentNewState + } + updateAssistant(update) }, 100 ) - }, [setTimeoutTimer, updateAssistant, assistant, urlContentNewState]) + }, [setTimeoutTimer, assistant, urlContentNewState, updateAssistant, t]) return ( diff --git a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx index 3d84ef690a..9f71e0a84d 100644 --- a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx @@ -1,15 +1,20 @@ import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons' +import { loggerService } from '@logger' import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo } from '@renderer/components/Icons' import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel' -import { isWebSearchModel } from '@renderer/config/models' +import { isGeminiModel, isWebSearchModel } from '@renderer/config/models' +import { isGeminiWebSearchProvider } from '@renderer/config/providers' import { useAssistant } from '@renderer/hooks/useAssistant' +import { useTimer } from '@renderer/hooks/useTimer' import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders' +import { getProviderByModel } from '@renderer/services/AssistantService' import WebSearchService from '@renderer/services/WebSearchService' import { Assistant, WebSearchProvider, WebSearchProviderId } from '@renderer/types' import { hasObjectKey } from '@renderer/utils' +import { isToolUseModeFunction } from '@renderer/utils/assistant' import { Tooltip } from 'antd' import { Globe } from 'lucide-react' -import { FC, memo, startTransition, useCallback, useImperativeHandle, useMemo } from 'react' +import { FC, memo, useCallback, useImperativeHandle, useMemo } from 'react' import { useTranslation } from 'react-i18next' export interface WebSearchButtonRef { @@ -22,11 +27,14 @@ interface Props { ToolbarButton: any } +const logger = loggerService.withContext('WebSearchButton') + const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { const { t } = useTranslation() const quickPanel = useQuickPanel() const { providers } = useWebSearchProviders() const { updateAssistant } = useAssistant(assistant.id) + const { setTimeoutTimer } = useTimer() // 注意:assistant.enableWebSearch 有不同的语义 /** 表示是否启用网络搜索 */ @@ -54,13 +62,12 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { return } }, - [enableWebSearch] + [] ) const updateWebSearchProvider = useCallback( async (providerId?: WebSearchProvider['id']) => { - // TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿 - startTransition(() => { + setTimeoutTimer('updateWebSearchProvider', () => { updateAssistant({ ...assistant, webSearchProviderId: providerId, @@ -68,12 +75,11 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { }) }) }, - [assistant, updateAssistant] + [assistant, setTimeoutTimer, updateAssistant] ) const updateQuickPanelItem = useCallback( async (providerId?: WebSearchProvider['id']) => { - // TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿 if (providerId === assistant.webSearchProviderId) { updateWebSearchProvider(undefined) } else { @@ -84,11 +90,31 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { ) const updateToModelBuiltinWebSearch = useCallback(async () => { - // TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿 - startTransition(() => { - updateAssistant({ ...assistant, webSearchProviderId: undefined, enableWebSearch: !assistant.enableWebSearch }) - }) - }, [assistant, updateAssistant]) + const update = { + ...assistant, + webSearchProviderId: undefined, + enableWebSearch: !assistant.enableWebSearch + } + const model = assistant.model + const provider = getProviderByModel(model) + if (!model) { + logger.error('Model does not exist.') + window.message.error(t('error.model.not_exists')) + return + } + if ( + isGeminiWebSearchProvider(provider) && + isGeminiModel(model) && + isToolUseModeFunction(assistant) && + update.enableWebSearch && + assistant.mcpServers && + assistant.mcpServers.length > 0 + ) { + update.enableWebSearch = false + window.message.warning(t('chat.mcp.warning.gemini_web_search')) + } + setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200) + }, [assistant, setTimeoutTimer, t, updateAssistant]) const providerItems = useMemo(() => { const isWebSearchModelEnabled = assistant.model && isWebSearchModel(assistant.model) diff --git a/src/renderer/src/utils/__tests__/assistant.test.ts b/src/renderer/src/utils/__tests__/assistant.test.ts new file mode 100644 index 0000000000..1bc6fca8e3 --- /dev/null +++ b/src/renderer/src/utils/__tests__/assistant.test.ts @@ -0,0 +1,40 @@ +import { Assistant } from '@renderer/types' +import { cloneDeep } from 'lodash' +import { describe, expect, it } from 'vitest' + +import { isToolUseModeFunction } from '../assistant' + +describe('assistant', () => { + const assistant: Assistant = { + id: 'assistant', + name: 'assistant', + prompt: '', + topics: [], + type: '' + } + + describe('isToolUseModeFunction', () => { + it('should detect function tool use mode', () => { + const mockAssistant = cloneDeep(assistant) + mockAssistant.settings = { toolUseMode: 'function' } + expect(isToolUseModeFunction(mockAssistant)).toBe(true) + }) + + it('should detect non-function tool use mode', () => { + const mockAssistant = cloneDeep(assistant) + mockAssistant.settings = { toolUseMode: 'prompt' } + expect(isToolUseModeFunction(mockAssistant)).toBe(false) + }) + + it('should handle undefined settings', () => { + const mockAssistant = cloneDeep(assistant) + expect(isToolUseModeFunction(mockAssistant)).toBe(false) + }) + + it('should handle undefined toolUseMode', () => { + const mockAssistant = cloneDeep(assistant) + mockAssistant.settings = {} + expect(isToolUseModeFunction(mockAssistant)).toBe(false) + }) + }) +}) diff --git a/src/renderer/src/utils/assistant.ts b/src/renderer/src/utils/assistant.ts new file mode 100644 index 0000000000..e08214aee7 --- /dev/null +++ b/src/renderer/src/utils/assistant.ts @@ -0,0 +1,5 @@ +import { Assistant } from '@renderer/types' + +export const isToolUseModeFunction = (assistant: Assistant) => { + return assistant.settings?.toolUseMode === 'function' +} diff --git a/src/renderer/src/utils/mcp-tools.ts b/src/renderer/src/utils/mcp-tools.ts index 6534c2e103..3be44b1091 100644 --- a/src/renderer/src/utils/mcp-tools.ts +++ b/src/renderer/src/utils/mcp-tools.ts @@ -28,6 +28,7 @@ import { ChatCompletionTool } from 'openai/resources' +import { isToolUseModeFunction } from './assistant' import { convertBase64ImageToAwsBedrockFormat } from './aws-bedrock-utils' import { filterProperties, processSchemaForO3 } from './mcp-schema' @@ -823,9 +824,7 @@ export function mcpToolCallResponseToAwsBedrockMessage( export function isEnabledToolUse(assistant: Assistant) { if (assistant.model) { - if (isFunctionCallingModel(assistant.model)) { - return assistant.settings?.toolUseMode === 'function' - } + return isFunctionCallingModel(assistant.model) && isToolUseModeFunction(assistant) } return false