Feat: url context for Gemini models (#7931)

* feat: Add URL Context ability for Gemini Models

* feat: Adding URL Context Button to tool bar and make it visible only when gemini models selected.
It is not working (adding urlContext tools) for now.

* fix: trying to force enable UrlContext function

* fix: enableUrlContext indication reverted

* feat: migration script for refreshing tool order to add URL Context button.

* fix: optimize migrate.ts

* fix: upgrade version

---------

Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
ous50 | ousfifty | 欧式fifty 2025-07-14 23:52:19 +08:00 committed by GitHub
parent 1b129636ed
commit 71917eb0ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 117 additions and 2 deletions

View File

@ -443,7 +443,7 @@ export class GeminiAPIClient extends BaseApiClient<
messages: GeminiSdkMessageParam[] messages: GeminiSdkMessageParam[]
metadata: Record<string, any> metadata: Record<string, any>
}> => { }> => {
const { messages, mcpTools, maxTokens, enableWebSearch, enableGenerateImage } = coreRequest const { messages, mcpTools, maxTokens, enableWebSearch, enableUrlContext, enableGenerateImage } = coreRequest
// 1. 处理系统消息 // 1. 处理系统消息
let systemInstruction = assistant.prompt let systemInstruction = assistant.prompt
@ -483,6 +483,12 @@ export class GeminiAPIClient extends BaseApiClient<
}) })
} }
if (enableUrlContext) {
tools.push({
urlContext: {}
})
}
if (isGemmaModel(model) && assistant.prompt) { if (isGemmaModel(model) && assistant.prompt) {
const isFirstMessage = history.length === 0 const isFirstMessage = history.length === 0
if (isFirstMessage && messageContents) { if (isFirstMessage && messageContents) {

View File

@ -84,6 +84,7 @@ export interface ResponseChunkTransformerContext {
isStreaming: boolean isStreaming: boolean
isEnabledToolCalling: boolean isEnabledToolCalling: boolean
isEnabledWebSearch: boolean isEnabledWebSearch: boolean
isEnabledUrlContext: boolean
isEnabledReasoning: boolean isEnabledReasoning: boolean
mcpTools: MCPTool[] mcpTools: MCPTool[]
provider: Provider provider: Provider

View File

@ -55,6 +55,7 @@ export const ResponseTransformMiddleware: CompletionsMiddleware =
isStreaming: params.streamOutput || false, isStreaming: params.streamOutput || false,
isEnabledToolCalling: (params.mcpTools && params.mcpTools.length > 0) || false, isEnabledToolCalling: (params.mcpTools && params.mcpTools.length > 0) || false,
isEnabledWebSearch: params.enableWebSearch || false, isEnabledWebSearch: params.enableWebSearch || false,
isEnabledUrlContext: params.enableUrlContext || false,
isEnabledReasoning: params.enableReasoning || false, isEnabledReasoning: params.enableReasoning || false,
mcpTools: params.mcpTools || [], mcpTools: params.mcpTools || [],
provider: ctx.apiClientInstance?.provider provider: ctx.apiClientInstance?.provider

View File

@ -49,6 +49,7 @@ export interface CompletionsParams {
// 功能开关 // 功能开关
streamOutput: boolean streamOutput: boolean
enableWebSearch?: boolean enableWebSearch?: boolean
enableUrlContext?: boolean
enableReasoning?: boolean enableReasoning?: boolean
enableGenerateImage?: boolean enableGenerateImage?: boolean

View File

@ -39,3 +39,18 @@ export function getWebSearchTools(model: Model): ChatCompletionTool[] {
} }
return [] return []
} }
export function getUrlContextTools(model: Model): ChatCompletionTool[] {
if (model.id.includes('gemini')) {
return [
{
type: 'function',
function: {
name: 'urlContext'
}
}
]
}
return []
}

View File

@ -211,6 +211,7 @@
"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",
"input.url_context": "URL Context",
"input.web_search.no_web_search": "Disable Web Search", "input.web_search.no_web_search": "Disable Web Search",
"input.web_search.no_web_search.description": "Do not enable web search", "input.web_search.no_web_search.description": "Do not enable web search",
"input.web_search.settings": "Web Search Settings", "input.web_search.settings": "Web Search Settings",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "ウェブ検索を無効にする", "input.web_search.no_web_search": "ウェブ検索を無効にする",
"input.web_search.no_web_search.description": "ウェブ検索を無効にする", "input.web_search.no_web_search.description": "ウェブ検索を無効にする",
"input.web_search.settings": "ウェブ検索設定", "input.web_search.settings": "ウェブ検索設定",
"input.url_context": "URLコンテキスト",
"message.new.branch": "新しいブランチ", "message.new.branch": "新しいブランチ",
"message.new.branch.created": "新しいブランチが作成されました", "message.new.branch.created": "新しいブランチが作成されました",
"message.new.context": "新しいコンテキスト", "message.new.context": "新しいコンテキスト",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "Отключить веб-поиск", "input.web_search.no_web_search": "Отключить веб-поиск",
"input.web_search.no_web_search.description": "Отключить веб-поиск", "input.web_search.no_web_search.description": "Отключить веб-поиск",
"input.web_search.settings": "Настройки веб-поиска", "input.web_search.settings": "Настройки веб-поиска",
"input.url_context": "Контекст страницы",
"message.new.branch": "Новая ветка", "message.new.branch": "Новая ветка",
"message.new.branch.created": "Новая ветка создана", "message.new.branch.created": "Новая ветка создана",
"message.new.context": "Новый контекст", "message.new.context": "Новый контекст",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "不使用网络", "input.web_search.no_web_search": "不使用网络",
"input.web_search.no_web_search.description": "不启用网络搜索功能", "input.web_search.no_web_search.description": "不启用网络搜索功能",
"input.web_search.settings": "网络搜索设置", "input.web_search.settings": "网络搜索设置",
"input.url_context": "网页上下文",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已创建", "message.new.branch.created": "新分支已创建",
"message.new.context": "清除上下文", "message.new.context": "清除上下文",

View File

@ -214,6 +214,7 @@
"input.web_search.no_web_search": "關閉網路搜尋", "input.web_search.no_web_search": "關閉網路搜尋",
"input.web_search.no_web_search.description": "關閉網路搜尋", "input.web_search.no_web_search.description": "關閉網路搜尋",
"input.web_search.settings": "網路搜尋設定", "input.web_search.settings": "網路搜尋設定",
"input.url_context": "網頁上下文",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已建立", "message.new.branch.created": "新分支已建立",
"message.new.context": "新上下文", "message.new.context": "新上下文",

View File

@ -14,6 +14,7 @@ import {
FileSearch, FileSearch,
Globe, Globe,
Languages, Languages,
Link,
LucideSquareTerminal, LucideSquareTerminal,
Maximize, Maximize,
MessageSquareDiff, MessageSquareDiff,
@ -36,6 +37,7 @@ import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButt
import NewContextButton from './NewContextButton' import NewContextButton from './NewContextButton'
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton' import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton' import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton'
import UrlContextButton, { UrlContextButtonRef } from './UrlContextbutton'
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton' import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'
export interface InputbarToolsRef { export interface InputbarToolsRef {
@ -128,6 +130,7 @@ const InputbarTools = ({
const attachmentButtonRef = useRef<AttachmentButtonRef>(null) const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null) const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null)
const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null) const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null)
const urlContextButtonRef = useRef<UrlContextButtonRef | null>(null)
const toolOrder = useAppSelector((state) => state.inputTools.toolOrder) const toolOrder = useAppSelector((state) => state.inputTools.toolOrder)
const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed) const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed)
@ -230,6 +233,15 @@ const InputbarTools = ({
webSearchButtonRef.current?.openQuickPanel() webSearchButtonRef.current?.openQuickPanel()
} }
}, },
{
label: t('chat.input.url_context'),
description: '',
icon: <Link />,
isMenu: true,
action: () => {
urlContextButtonRef.current?.openQuickPanel()
}
},
{ {
label: couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document'), label: couldAddImageFile ? t('chat.input.upload') : t('chat.input.upload.document'),
description: '', description: '',
@ -328,6 +340,12 @@ const InputbarTools = ({
label: t('chat.input.web_search'), label: t('chat.input.web_search'),
component: <WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} /> component: <WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
}, },
{
key: 'url_context',
label: t('chat.input.url_context'),
component: <UrlContextButton ref={urlContextButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />,
condition: model.id.toLowerCase().includes('gemini')
},
{ {
key: 'knowledge_base', key: 'knowledge_base',
label: t('chat.input.knowledge_base'), label: t('chat.input.knowledge_base'),

View File

@ -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<UrlContextButtonRef | null>
assistant: Assistant
ToolbarButton: any
}
const UrlContextButton: FC<Props> = ({ 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 (
<Tooltip placement="top" title={t('chat.input.url_context')} arrow>
<ToolbarButton type="text" onClick={handleToggle}>
<Link
size={18}
style={{
color: assistant.enableUrlContext ? 'var(--color-link)' : 'var(--color-icon)'
}}
/>
</ToolbarButton>
</Tooltip>
)
}
export default memo(UrlContextButton)

View File

@ -354,6 +354,8 @@ export async function fetchChatCompletion({
model.id.includes('sonar') || model.id.includes('sonar') ||
false false
const enableUrlContext = assistant.enableUrlContext || false
const enableGenerateImage = const enableGenerateImage =
isGenerateImageModel(model) && (isSupportedDisableGenerationModel(model) ? assistant.enableGenerateImage : true) isGenerateImageModel(model) && (isSupportedDisableGenerationModel(model) ? assistant.enableGenerateImage : true)
@ -370,6 +372,7 @@ export async function fetchChatCompletion({
streamOutput: assistant.settings?.streamOutput || false, streamOutput: assistant.settings?.streamOutput || false,
enableReasoning, enableReasoning,
enableWebSearch, enableWebSearch,
enableUrlContext,
enableGenerateImage enableGenerateImage
}, },
{ {

View File

@ -54,7 +54,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 120, version: 121,
blacklist: ['runtime', 'messages', 'messageBlocks'], blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate migrate
}, },

View File

@ -11,6 +11,7 @@ export const DEFAULT_TOOL_ORDER: ToolOrder = {
'attachment', 'attachment',
'thinking', 'thinking',
'web_search', 'web_search',
'url_context',
'knowledge_base', 'knowledge_base',
'mcp_tools', 'mcp_tools',
'generate_image', 'generate_image',

View File

@ -1763,6 +1763,24 @@ const migrateConfig = {
} catch (error) { } catch (error) {
return state 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
}
} }
} }

View File

@ -23,6 +23,8 @@ export type Assistant = {
/** enableWebSearch 代表使用模型内置网络搜索功能 */ /** enableWebSearch 代表使用模型内置网络搜索功能 */
enableWebSearch?: boolean enableWebSearch?: boolean
webSearchProviderId?: WebSearchProvider['id'] webSearchProviderId?: WebSearchProvider['id']
// enableUrlContext 是 Gemini 的特有功能
enableUrlContext?: boolean
enableGenerateImage?: boolean enableGenerateImage?: boolean
mcpServers?: MCPServer[] mcpServers?: MCPServer[]
knowledgeRecognition?: 'off' | 'on' knowledgeRecognition?: 'off' | 'on'