mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 20:00:00 +08:00
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:
parent
1b129636ed
commit
71917eb0ec
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 []
|
||||||
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "新しいコンテキスト",
|
||||||
|
|||||||
@ -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": "Новый контекст",
|
||||||
|
|||||||
@ -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": "清除上下文",
|
||||||
|
|||||||
@ -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": "新上下文",
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
44
src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx
Normal file
44
src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx
Normal 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)
|
||||||
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user