diff --git a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts index bd87ccc821..30d497c21d 100644 --- a/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts +++ b/src/renderer/src/aiCore/clients/gemini/GeminiAPIClient.ts @@ -443,7 +443,7 @@ export class GeminiAPIClient extends BaseApiClient< messages: GeminiSdkMessageParam[] metadata: Record }> => { - const { messages, mcpTools, maxTokens, enableWebSearch, enableGenerateImage } = coreRequest + const { messages, mcpTools, maxTokens, enableWebSearch, enableUrlContext, enableGenerateImage } = coreRequest // 1. 处理系统消息 let systemInstruction = assistant.prompt @@ -483,6 +483,12 @@ export class GeminiAPIClient extends BaseApiClient< }) } + if (enableUrlContext) { + tools.push({ + urlContext: {} + }) + } + if (isGemmaModel(model) && assistant.prompt) { const isFirstMessage = history.length === 0 if (isFirstMessage && messageContents) { diff --git a/src/renderer/src/aiCore/clients/types.ts b/src/renderer/src/aiCore/clients/types.ts index ff03f10d38..a6f4adc38d 100644 --- a/src/renderer/src/aiCore/clients/types.ts +++ b/src/renderer/src/aiCore/clients/types.ts @@ -84,6 +84,7 @@ export interface ResponseChunkTransformerContext { isStreaming: boolean isEnabledToolCalling: boolean isEnabledWebSearch: boolean + isEnabledUrlContext: boolean isEnabledReasoning: boolean mcpTools: MCPTool[] provider: Provider diff --git a/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts b/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts index eccbe86bdd..5477aa6557 100644 --- a/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts @@ -55,6 +55,7 @@ export const ResponseTransformMiddleware: CompletionsMiddleware = isStreaming: params.streamOutput || false, isEnabledToolCalling: (params.mcpTools && params.mcpTools.length > 0) || false, isEnabledWebSearch: params.enableWebSearch || false, + isEnabledUrlContext: params.enableUrlContext || false, isEnabledReasoning: params.enableReasoning || false, mcpTools: params.mcpTools || [], provider: ctx.apiClientInstance?.provider diff --git a/src/renderer/src/aiCore/middleware/schemas.ts b/src/renderer/src/aiCore/middleware/schemas.ts index 33d9816b4f..75247443c2 100644 --- a/src/renderer/src/aiCore/middleware/schemas.ts +++ b/src/renderer/src/aiCore/middleware/schemas.ts @@ -49,6 +49,7 @@ export interface CompletionsParams { // 功能开关 streamOutput: boolean enableWebSearch?: boolean + enableUrlContext?: boolean enableReasoning?: boolean enableGenerateImage?: boolean diff --git a/src/renderer/src/config/tools.ts b/src/renderer/src/config/tools.ts index dcb16b8033..18e2d62134 100644 --- a/src/renderer/src/config/tools.ts +++ b/src/renderer/src/config/tools.ts @@ -39,3 +39,18 @@ export function getWebSearchTools(model: Model): ChatCompletionTool[] { } return [] } + +export function getUrlContextTools(model: Model): ChatCompletionTool[] { + if (model.id.includes('gemini')) { + return [ + { + type: 'function', + function: { + name: 'urlContext' + } + } + ] + } + + return [] +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 68e652ac73..4c985c8954 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -211,6 +211,7 @@ "input.web_search.button.ok": "Go to Settings", "input.web_search.enable": "Enable web search", "input.web_search.enable_content": "Need to check web search connectivity in settings first", + "input.url_context": "URL Context", "input.web_search.no_web_search": "Disable Web Search", "input.web_search.no_web_search.description": "Do not enable web search", "input.web_search.settings": "Web Search Settings", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 83db85c5d4..776c06ad1b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -214,6 +214,7 @@ "input.web_search.no_web_search": "ウェブ検索を無効にする", "input.web_search.no_web_search.description": "ウェブ検索を無効にする", "input.web_search.settings": "ウェブ検索設定", + "input.url_context": "URLコンテキスト", "message.new.branch": "新しいブランチ", "message.new.branch.created": "新しいブランチが作成されました", "message.new.context": "新しいコンテキスト", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index f51ad78e88..1b5a500b97 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -214,6 +214,7 @@ "input.web_search.no_web_search": "Отключить веб-поиск", "input.web_search.no_web_search.description": "Отключить веб-поиск", "input.web_search.settings": "Настройки веб-поиска", + "input.url_context": "Контекст страницы", "message.new.branch": "Новая ветка", "message.new.branch.created": "Новая ветка создана", "message.new.context": "Новый контекст", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 76121e7f51..902b2362b5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -214,6 +214,7 @@ "input.web_search.no_web_search": "不使用网络", "input.web_search.no_web_search.description": "不启用网络搜索功能", "input.web_search.settings": "网络搜索设置", + "input.url_context": "网页上下文", "message.new.branch": "分支", "message.new.branch.created": "新分支已创建", "message.new.context": "清除上下文", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index d4bada9a36..3346349544 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -214,6 +214,7 @@ "input.web_search.no_web_search": "關閉網路搜尋", "input.web_search.no_web_search.description": "關閉網路搜尋", "input.web_search.settings": "網路搜尋設定", + "input.url_context": "網頁上下文", "message.new.branch": "分支", "message.new.branch.created": "新分支已建立", "message.new.context": "新上下文", diff --git a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx index 7609b8cfe3..d9b9dcf539 100644 --- a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx +++ b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx @@ -14,6 +14,7 @@ import { FileSearch, Globe, Languages, + Link, LucideSquareTerminal, Maximize, MessageSquareDiff, @@ -36,6 +37,7 @@ import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButt import NewContextButton from './NewContextButton' import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton' import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton' +import UrlContextButton, { UrlContextButtonRef } from './UrlContextbutton' import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton' export interface InputbarToolsRef { @@ -128,6 +130,7 @@ const InputbarTools = ({ const attachmentButtonRef = useRef(null) const webSearchButtonRef = useRef(null) const thinkingButtonRef = useRef(null) + const urlContextButtonRef = useRef(null) const toolOrder = useAppSelector((state) => state.inputTools.toolOrder) const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed) @@ -230,6 +233,15 @@ const InputbarTools = ({ webSearchButtonRef.current?.openQuickPanel() } }, + { + label: t('chat.input.url_context'), + description: '', + icon: , + isMenu: true, + action: () => { + urlContextButtonRef.current?.openQuickPanel() + } + }, { label: couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document'), description: '', @@ -328,6 +340,12 @@ const InputbarTools = ({ label: t('chat.input.web_search'), component: }, + { + key: 'url_context', + label: t('chat.input.url_context'), + component: , + condition: model.id.toLowerCase().includes('gemini') + }, { key: 'knowledge_base', label: t('chat.input.knowledge_base'), diff --git a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx new file mode 100644 index 0000000000..2f5a228bb8 --- /dev/null +++ b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx @@ -0,0 +1,44 @@ +import { useAssistant } from '@renderer/hooks/useAssistant' +import { Assistant } from '@renderer/types' +import { Tooltip } from 'antd' +import { Link } from 'lucide-react' +import { FC, memo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' + +export interface UrlContextButtonRef { + openQuickPanel: () => void +} + +interface Props { + ref?: React.RefObject + assistant: Assistant + ToolbarButton: any +} + +const UrlContextButton: FC = ({ assistant, ToolbarButton }) => { + const { t } = useTranslation() + const { updateAssistant } = useAssistant(assistant.id) + + const urlContentNewState = !assistant.enableUrlContext + + const handleToggle = useCallback(() => { + setTimeout(() => { + updateAssistant({ ...assistant, enableUrlContext: urlContentNewState }) + }, 100) + }, [assistant, urlContentNewState, updateAssistant]) + + return ( + + + + + + ) +} + +export default memo(UrlContextButton) diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 50721de77a..7717dd60f9 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -354,6 +354,8 @@ export async function fetchChatCompletion({ model.id.includes('sonar') || false + const enableUrlContext = assistant.enableUrlContext || false + const enableGenerateImage = isGenerateImageModel(model) && (isSupportedDisableGenerationModel(model) ? assistant.enableGenerateImage : true) @@ -370,6 +372,7 @@ export async function fetchChatCompletion({ streamOutput: assistant.settings?.streamOutput || false, enableReasoning, enableWebSearch, + enableUrlContext, enableGenerateImage }, { diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 5f1d84bfae..243716282f 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -54,7 +54,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 120, + version: 121, blacklist: ['runtime', 'messages', 'messageBlocks'], migrate }, diff --git a/src/renderer/src/store/inputTools.ts b/src/renderer/src/store/inputTools.ts index 94c9dfb3ef..be6ef212c0 100644 --- a/src/renderer/src/store/inputTools.ts +++ b/src/renderer/src/store/inputTools.ts @@ -11,6 +11,7 @@ export const DEFAULT_TOOL_ORDER: ToolOrder = { 'attachment', 'thinking', 'web_search', + 'url_context', 'knowledge_base', 'mcp_tools', 'generate_image', diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index a4a7e292ac..cac8b071d5 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -1763,6 +1763,24 @@ const migrateConfig = { } catch (error) { return state } + }, + '121': (state: RootState) => { + try { + const { toolOrder } = state.inputTools + const urlContextKey = 'url_context' + const webSearchIndex = toolOrder.visible.indexOf('web_search') + const knowledgeBaseIndex = toolOrder.visible.indexOf('knowledge_base') + if (webSearchIndex !== -1) { + toolOrder.visible.splice(webSearchIndex, 0, urlContextKey) + } else if (knowledgeBaseIndex !== -1) { + toolOrder.visible.splice(knowledgeBaseIndex, 0, urlContextKey) + } else { + toolOrder.visible.push(urlContextKey) + } + return state + } catch (error) { + return state + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 58876962e7..42c878e8f9 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -23,6 +23,8 @@ export type Assistant = { /** enableWebSearch 代表使用模型内置网络搜索功能 */ enableWebSearch?: boolean webSearchProviderId?: WebSearchProvider['id'] + // enableUrlContext 是 Gemini 的特有功能 + enableUrlContext?: boolean enableGenerateImage?: boolean mcpServers?: MCPServer[] knowledgeRecognition?: 'off' | 'on'