Merge remote-tracking branch 'origin/main' into feat/agents-new
This commit is contained in:
@@ -51,6 +51,8 @@ class CodeToolsService {
|
||||
return '@openai/codex'
|
||||
case codeTools.qwenCode:
|
||||
return '@qwen-code/qwen-code'
|
||||
case codeTools.iFlowCli:
|
||||
return '@iflow-ai/iflow-cli'
|
||||
default:
|
||||
throw new Error(`Unsupported CLI tool: ${cliTool}`)
|
||||
}
|
||||
@@ -66,6 +68,8 @@ class CodeToolsService {
|
||||
return 'codex'
|
||||
case codeTools.qwenCode:
|
||||
return 'qwen'
|
||||
case codeTools.iFlowCli:
|
||||
return 'iflow'
|
||||
default:
|
||||
throw new Error(`Unsupported CLI tool: ${cliTool}`)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import type { MCPTool, Message, Model, Provider } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
@@ -26,6 +27,8 @@ export interface AiSdkMiddlewareConfig {
|
||||
enableUrlContext: boolean
|
||||
mcpTools?: MCPTool[]
|
||||
uiMessages?: Message[]
|
||||
// 内置搜索配置
|
||||
webSearchPluginConfig?: WebSearchPluginConfig
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +140,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
const tagNameArray = ['think', 'thought']
|
||||
const tagNameArray = ['think', 'thought', 'reasoning']
|
||||
|
||||
/**
|
||||
* 添加provider特定的中间件
|
||||
@@ -164,6 +167,16 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
case 'gemini':
|
||||
// Gemini特定中间件
|
||||
break
|
||||
case 'aws-bedrock': {
|
||||
if (config.model?.id.includes('gpt-oss')) {
|
||||
const tagName = tagNameArray[2]
|
||||
builder.add({
|
||||
name: 'thinking-tag-extraction',
|
||||
middleware: extractReasoningMiddleware({ tagName })
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
// 其他provider的通用处理
|
||||
break
|
||||
|
||||
@@ -30,9 +30,8 @@ export function buildPlugins(
|
||||
}
|
||||
|
||||
// 1. 模型内置搜索
|
||||
if (middlewareConfig.enableWebSearch) {
|
||||
// 内置了默认搜索参数,如果改的话可以传config进去
|
||||
plugins.push(webSearchPlugin())
|
||||
if (middlewareConfig.enableWebSearch && middlewareConfig.webSearchPluginConfig) {
|
||||
plugins.push(webSearchPlugin(middlewareConfig.webSearchPluginConfig))
|
||||
}
|
||||
// 2. 支持工具调用时添加搜索插件
|
||||
if (middlewareConfig.isSupportedToolUse || middlewareConfig.isPromptToolUse) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
|
||||
import { vertex } from '@ai-sdk/google-vertex/edge'
|
||||
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { isBaseProvider } from '@cherrystudio/ai-core/core/providers/schemas'
|
||||
import { loggerService } from '@logger'
|
||||
import {
|
||||
isGenerateImageModel,
|
||||
@@ -16,8 +18,11 @@ import {
|
||||
isWebSearchModel
|
||||
} from '@renderer/config/models'
|
||||
import { getAssistantSettings, getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import store from '@renderer/store'
|
||||
import { CherryWebSearchConfig } from '@renderer/store/websearch'
|
||||
import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
|
||||
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
||||
import type { ModelMessage, Tool } from 'ai'
|
||||
import { stepCountIs } from 'ai'
|
||||
|
||||
@@ -25,6 +30,7 @@ import { getAiSdkProviderId } from '../provider/factory'
|
||||
import { setupToolsConfig } from '../utils/mcp'
|
||||
import { buildProviderOptions } from '../utils/options'
|
||||
import { getAnthropicThinkingBudget } from '../utils/reasoning'
|
||||
import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch'
|
||||
import { getTemperature, getTopP } from './modelParameters'
|
||||
|
||||
const logger = loggerService.withContext('parameterBuilder')
|
||||
@@ -42,6 +48,7 @@ export async function buildStreamTextParams(
|
||||
options: {
|
||||
mcpTools?: MCPTool[]
|
||||
webSearchProviderId?: string
|
||||
webSearchConfig?: CherryWebSearchConfig
|
||||
requestOptions?: {
|
||||
signal?: AbortSignal
|
||||
timeout?: number
|
||||
@@ -57,6 +64,7 @@ export async function buildStreamTextParams(
|
||||
enableGenerateImage: boolean
|
||||
enableUrlContext: boolean
|
||||
}
|
||||
webSearchPluginConfig?: WebSearchPluginConfig
|
||||
}> {
|
||||
const { mcpTools } = options
|
||||
|
||||
@@ -93,6 +101,12 @@ export async function buildStreamTextParams(
|
||||
// }
|
||||
|
||||
// 构建真正的 providerOptions
|
||||
const webSearchConfig: CherryWebSearchConfig = {
|
||||
maxResults: store.getState().websearch.maxResults,
|
||||
excludeDomains: store.getState().websearch.excludeDomains,
|
||||
searchWithTime: store.getState().websearch.searchWithTime
|
||||
}
|
||||
|
||||
const providerOptions = buildProviderOptions(assistant, model, provider, {
|
||||
enableReasoning,
|
||||
enableWebSearch,
|
||||
@@ -109,15 +123,21 @@ export async function buildStreamTextParams(
|
||||
maxTokens -= getAnthropicThinkingBudget(assistant, model)
|
||||
}
|
||||
|
||||
// google-vertex | google-vertex-anthropic
|
||||
let webSearchPluginConfig: WebSearchPluginConfig | undefined = undefined
|
||||
if (enableWebSearch) {
|
||||
if (isBaseProvider(aiSdkProviderId)) {
|
||||
webSearchPluginConfig = buildProviderBuiltinWebSearchConfig(aiSdkProviderId, webSearchConfig)
|
||||
}
|
||||
if (!tools) {
|
||||
tools = {}
|
||||
}
|
||||
if (aiSdkProviderId === 'google-vertex') {
|
||||
tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool
|
||||
} else if (aiSdkProviderId === 'google-vertex-anthropic') {
|
||||
tools.web_search = vertexAnthropic.tools.webSearch_20250305({}) as ProviderDefinedTool
|
||||
tools.web_search = vertexAnthropic.tools.webSearch_20250305({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
}) as ProviderDefinedTool
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +171,8 @@ export async function buildStreamTextParams(
|
||||
return {
|
||||
params,
|
||||
modelId: model.id,
|
||||
capabilities: { enableReasoning, enableWebSearch, enableGenerateImage, enableUrlContext }
|
||||
capabilities: { enableReasoning, enableWebSearch, enableGenerateImage, enableUrlContext },
|
||||
webSearchPluginConfig
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ export function getAiSdkProviderId(provider: Provider): ProviderId | 'openai-com
|
||||
return resolvedFromType
|
||||
}
|
||||
}
|
||||
if (provider.apiHost.includes('api.openai.com')) {
|
||||
return 'openai-chat'
|
||||
}
|
||||
// 3. 最后的fallback(通常会成为openai-compatible)
|
||||
return provider.id as ProviderId
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export function buildProviderOptions(
|
||||
// 应该覆盖所有类型
|
||||
switch (baseProviderId) {
|
||||
case 'openai':
|
||||
case 'openai-chat':
|
||||
case 'azure':
|
||||
providerSpecificOptions = {
|
||||
...buildOpenAIProviderOptions(assistant, model, capabilities),
|
||||
@@ -101,13 +102,15 @@ export function buildProviderOptions(
|
||||
providerSpecificOptions = buildXAIProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
case 'deepseek':
|
||||
case 'openai-compatible':
|
||||
case 'openrouter':
|
||||
case 'openai-compatible': {
|
||||
// 对于其他 provider,使用通用的构建逻辑
|
||||
providerSpecificOptions = {
|
||||
...buildGenericProviderOptions(assistant, model, capabilities),
|
||||
serviceTier: serviceTierSetting
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported base provider ${baseProviderId}`)
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re
|
||||
|
||||
export function getAnthropicThinkingBudget(assistant: Assistant, model: Model): number {
|
||||
const { maxTokens, reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
||||
if (maxTokens === undefined || reasoningEffort === undefined) {
|
||||
if (reasoningEffort === undefined) {
|
||||
return 0
|
||||
}
|
||||
const effortRatio = EFFORT_RATIO[reasoningEffort]
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import {
|
||||
AnthropicSearchConfig,
|
||||
OpenAISearchConfig,
|
||||
WebSearchPluginConfig
|
||||
} from '@cherrystudio/ai-core/core/plugins/built-in/webSearchPlugin/helper'
|
||||
import { BaseProviderId } from '@cherrystudio/ai-core/provider'
|
||||
import { isOpenAIWebSearchChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '@renderer/config/prompts'
|
||||
import { CherryWebSearchConfig } from '@renderer/store/websearch'
|
||||
import { Model } from '@renderer/types'
|
||||
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
||||
|
||||
export function getWebSearchParams(model: Model): Record<string, any> {
|
||||
if (model.provider === 'hunyuan') {
|
||||
@@ -21,11 +28,78 @@ export function getWebSearchParams(model: Model): Record<string, any> {
|
||||
web_search_options: {}
|
||||
}
|
||||
}
|
||||
|
||||
if (model.provider === 'openrouter') {
|
||||
return {
|
||||
plugins: [{ id: 'web', search_prompts: WEB_SEARCH_PROMPT_FOR_OPENROUTER }]
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* range in [0, 100]
|
||||
* @param maxResults
|
||||
*/
|
||||
function mapMaxResultToOpenAIContextSize(maxResults: number): OpenAISearchConfig['searchContextSize'] {
|
||||
if (maxResults <= 33) return 'low'
|
||||
if (maxResults <= 66) return 'medium'
|
||||
return 'high'
|
||||
}
|
||||
|
||||
export function buildProviderBuiltinWebSearchConfig(
|
||||
providerId: BaseProviderId,
|
||||
webSearchConfig: CherryWebSearchConfig
|
||||
): WebSearchPluginConfig {
|
||||
switch (providerId) {
|
||||
case 'openai': {
|
||||
return {
|
||||
openai: {
|
||||
searchContextSize: mapMaxResultToOpenAIContextSize(webSearchConfig.maxResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'openai-chat': {
|
||||
return {
|
||||
'openai-chat': {
|
||||
searchContextSize: mapMaxResultToOpenAIContextSize(webSearchConfig.maxResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'anthropic': {
|
||||
const anthropicSearchOptions: AnthropicSearchConfig = {
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
}
|
||||
return {
|
||||
anthropic: anthropicSearchOptions
|
||||
}
|
||||
}
|
||||
case 'xai': {
|
||||
return {
|
||||
xai: {
|
||||
maxSearchResults: webSearchConfig.maxResults,
|
||||
returnCitations: true,
|
||||
sources: [
|
||||
{
|
||||
type: 'web',
|
||||
excludedWebsites: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
},
|
||||
{ type: 'news' },
|
||||
{ type: 'x' }
|
||||
],
|
||||
mode: 'on'
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'openrouter': {
|
||||
return {
|
||||
openrouter: {
|
||||
plugins: [
|
||||
{
|
||||
id: 'web',
|
||||
max_results: webSearchConfig.maxResults
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported provider: ${providerId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,11 @@ export const isGPT5SeriesModel = (model: Model) => {
|
||||
return modelId.includes('gpt-5')
|
||||
}
|
||||
|
||||
export const isGPT5SeriesReasoningModel = (model: Model) => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return modelId.includes('gpt-5') && !modelId.includes('chat')
|
||||
}
|
||||
|
||||
export const isGeminiModel = (model: Model) => {
|
||||
const modelId = getLowerBaseModelName(model.id)
|
||||
return modelId.includes('gemini')
|
||||
|
||||
@@ -1020,7 +1020,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
},
|
||||
anthropic: {
|
||||
api: {
|
||||
url: 'https://api.anthropic.com/'
|
||||
url: 'https://api.anthropic.com'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://anthropic.com/',
|
||||
|
||||
@@ -38,7 +38,7 @@ export function useAppInit() {
|
||||
customCss,
|
||||
enableDataCollection
|
||||
} = useSettings()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
const { minappShow } = useRuntime()
|
||||
const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
@@ -102,16 +102,15 @@ export function useAppInit() {
|
||||
}, [language])
|
||||
|
||||
useEffect(() => {
|
||||
const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow
|
||||
const isMacTransparentWindow = windowStyle === 'transparent' && isMac
|
||||
|
||||
if (minappShow && isTopNavbar) {
|
||||
window.root.style.background =
|
||||
windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)'
|
||||
if (minappShow && isLeftNavbar) {
|
||||
window.root.style.background = isMacTransparentWindow ? 'var(--color-background)' : 'var(--navbar-background)'
|
||||
return
|
||||
}
|
||||
|
||||
window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||
}, [windowStyle, minappShow, theme, isTopNavbar])
|
||||
window.root.style.background = isMacTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||
}, [windowStyle, minappShow, theme, isLeftNavbar])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLocalAi) {
|
||||
|
||||
@@ -679,7 +679,12 @@
|
||||
"title": "Topics",
|
||||
"unpin": "Unpin Topic"
|
||||
},
|
||||
"translate": "Translate"
|
||||
"translate": "Translate",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "The GPT-5 model's minimal reasoning effort does not support web search."
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "Automatically update to latest version",
|
||||
|
||||
@@ -679,7 +679,12 @@
|
||||
"title": "话题",
|
||||
"unpin": "取消固定"
|
||||
},
|
||||
"translate": "翻译"
|
||||
"translate": "翻译",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "GPT5 模型 minimal 思考强度不支持网络搜索"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "检查更新并安装最新版本",
|
||||
|
||||
@@ -679,7 +679,12 @@
|
||||
"title": "話題",
|
||||
"unpin": "取消固定"
|
||||
},
|
||||
"translate": "翻譯"
|
||||
"translate": "翻譯",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "GPT-5 模型的最小推理力度不支援網路搜尋。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "檢查更新並安裝最新版本",
|
||||
|
||||
@@ -381,8 +381,9 @@
|
||||
"translate": "Μετάφραση στο {{target_language}}",
|
||||
"translating": "Μετάφραση...",
|
||||
"upload": {
|
||||
"attachment": "Μεταφόρτωση συνημμένου",
|
||||
"document": "Φόρτωση έγγραφου (το μοντέλο δεν υποστηρίζει εικόνες)",
|
||||
"label": "Φόρτωση εικόνας ή έγγραφου",
|
||||
"image_or_document": "Μεταφόρτωση εικόνας ή εγγράφου",
|
||||
"upload_from_local": "Μεταφόρτωση αρχείου από τον υπολογιστή..."
|
||||
},
|
||||
"url_context": "Περιεχόμενο ιστοσελίδας",
|
||||
@@ -678,7 +679,12 @@
|
||||
"title": "Θέματα",
|
||||
"unpin": "Ξεκαρφίτσωμα"
|
||||
},
|
||||
"translate": "Μετάφραση"
|
||||
"translate": "Μετάφραση",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "Το μοντέλο GPT5 με ελάχιστη ένταση σκέψης δεν υποστηρίζει αναζήτηση στο διαδίκτυο"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "Έλεγχος για ενημερώσεις και εγκατάσταση της τελευταίας έκδοσης",
|
||||
|
||||
@@ -381,8 +381,9 @@
|
||||
"translate": "Traducir a {{target_language}}",
|
||||
"translating": "Traduciendo...",
|
||||
"upload": {
|
||||
"attachment": "Subir archivo adjunto",
|
||||
"document": "Subir documento (el modelo no admite imágenes)",
|
||||
"label": "Subir imagen o documento",
|
||||
"image_or_document": "Subir imagen o documento",
|
||||
"upload_from_local": "Subir archivo local..."
|
||||
},
|
||||
"url_context": "Contexto de la página web",
|
||||
@@ -678,7 +679,12 @@
|
||||
"title": "Tema",
|
||||
"unpin": "Quitar fijación"
|
||||
},
|
||||
"translate": "Traducir"
|
||||
"translate": "Traducir",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "El modelo GPT5 con intensidad de pensamiento mínima no admite búsqueda en la web."
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "Comprobar actualizaciones e instalar la versión más reciente",
|
||||
|
||||
@@ -381,8 +381,9 @@
|
||||
"translate": "Traduire en {{target_language}}",
|
||||
"translating": "Traduction en cours...",
|
||||
"upload": {
|
||||
"attachment": "Télécharger la pièce jointe",
|
||||
"document": "Télécharger un document (le modèle ne prend pas en charge les images)",
|
||||
"label": "Télécharger une image ou un document",
|
||||
"image_or_document": "Télécharger une image ou un document",
|
||||
"upload_from_local": "Télécharger un fichier local..."
|
||||
},
|
||||
"url_context": "Contexte de la page web",
|
||||
@@ -678,7 +679,12 @@
|
||||
"title": "Sujet",
|
||||
"unpin": "Annuler le fixage"
|
||||
},
|
||||
"translate": "Traduire"
|
||||
"translate": "Traduire",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "Le modèle GPT5 avec une intensité de réflexion minimale ne prend pas en charge la recherche sur Internet."
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "Vérifier les mises à jour et installer la dernière version",
|
||||
|
||||
@@ -381,8 +381,9 @@
|
||||
"translate": "{{target_language}}に翻訳",
|
||||
"translating": "翻訳中...",
|
||||
"upload": {
|
||||
"attachment": "添付ファイルをアップロード",
|
||||
"document": "ドキュメントをアップロード(モデルは画像をサポートしません)",
|
||||
"label": "画像またはドキュメントをアップロード",
|
||||
"image_or_document": "画像またはドキュメントをアップロード",
|
||||
"upload_from_local": "ローカルファイルをアップロード..."
|
||||
},
|
||||
"url_context": "URLコンテキスト",
|
||||
@@ -678,7 +679,12 @@
|
||||
"title": "トピック",
|
||||
"unpin": "固定解除"
|
||||
},
|
||||
"translate": "翻訳"
|
||||
"translate": "翻訳",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "GPT5モデルの最小思考強度ではネット検索はサポートされません"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "最新バージョンを自動的に更新する",
|
||||
|
||||
@@ -381,8 +381,9 @@
|
||||
"translate": "Traduzir para {{target_language}}",
|
||||
"translating": "Traduzindo...",
|
||||
"upload": {
|
||||
"attachment": "Carregar anexo",
|
||||
"document": "Carregar documento (o modelo não suporta imagens)",
|
||||
"label": "Carregar imagem ou documento",
|
||||
"image_or_document": "Carregar imagem ou documento",
|
||||
"upload_from_local": "Fazer upload de arquivo local..."
|
||||
},
|
||||
"url_context": "Contexto da Página da Web",
|
||||
@@ -678,7 +679,12 @@
|
||||
"title": "Tópicos",
|
||||
"unpin": "Desfixar"
|
||||
},
|
||||
"translate": "Traduzir"
|
||||
"translate": "Traduzir",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "O modelo GPT5 com intensidade mínima de pensamento não suporta pesquisa na web"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "Verificar atualizações e instalar a versão mais recente",
|
||||
|
||||
@@ -381,8 +381,9 @@
|
||||
"translate": "Перевести на {{target_language}}",
|
||||
"translating": "Перевод...",
|
||||
"upload": {
|
||||
"attachment": "Загрузить вложение",
|
||||
"document": "Загрузить документ (модель не поддерживает изображения)",
|
||||
"label": "Загрузить изображение или документ",
|
||||
"image_or_document": "Загрузить изображение или документ",
|
||||
"upload_from_local": "Загрузить локальный файл..."
|
||||
},
|
||||
"url_context": "Контекст страницы",
|
||||
@@ -678,7 +679,12 @@
|
||||
"title": "Топики",
|
||||
"unpin": "Открепленные темы"
|
||||
},
|
||||
"translate": "Перевести"
|
||||
"translate": "Перевести",
|
||||
"web_search": {
|
||||
"warning": {
|
||||
"openai": "Модель GPT5 с минимальной интенсивностью мышления не поддерживает поиск в интернете"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"auto_update_to_latest": "Автоматически обновлять до последней версии",
|
||||
|
||||
@@ -19,7 +19,8 @@ export const CLI_TOOLS = [
|
||||
{ value: codeTools.claudeCode, label: 'Claude Code' },
|
||||
{ value: codeTools.qwenCode, label: 'Qwen Code' },
|
||||
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
|
||||
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
|
||||
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' },
|
||||
{ value: codeTools.iFlowCli, label: 'iFlow CLI' }
|
||||
]
|
||||
|
||||
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
||||
@@ -35,7 +36,8 @@ export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Pr
|
||||
providers.filter((p) => p.type === 'gemini' || GEMINI_SUPPORTED_PROVIDERS.includes(p.id)),
|
||||
[codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')),
|
||||
[codeTools.openaiCodex]: (providers) =>
|
||||
providers.filter((p) => p.id === 'openai' || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(p.id))
|
||||
providers.filter((p) => p.id === 'openai' || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(p.id)),
|
||||
[codeTools.iFlowCli]: (providers) => providers.filter((p) => p.type.includes('openai'))
|
||||
}
|
||||
|
||||
export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
|
||||
@@ -144,6 +146,12 @@ export const generateToolEnvironment = ({
|
||||
env.OPENAI_MODEL = model.id
|
||||
env.OPENAI_MODEL_PROVIDER = modelProvider.id
|
||||
break
|
||||
|
||||
case codeTools.iFlowCli:
|
||||
env.IFLOW_API_KEY = apiKey
|
||||
env.IFLOW_BASE_URL = baseUrl
|
||||
env.IFLOW_MODEL_NAME = model.id
|
||||
break
|
||||
}
|
||||
|
||||
return env
|
||||
|
||||
@@ -155,23 +155,23 @@ const Chat: FC<Props> = (props) => {
|
||||
flex={1}
|
||||
justify="space-between"
|
||||
style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
assistant={assistant}
|
||||
topic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
onComponentUpdate={messagesComponentUpdateHandler}
|
||||
onFirstUpdate={messagesComponentFirstUpdateHandler}
|
||||
/>
|
||||
<ContentSearch
|
||||
ref={contentSearchRef}
|
||||
searchTarget={mainRef as React.RefObject<HTMLElement>}
|
||||
filter={contentSearchFilter}
|
||||
includeUser={filterIncludeUser}
|
||||
onIncludeUserChange={userOutlinedItemClickHandler}
|
||||
/>
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
<QuickPanelProvider>
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
assistant={assistant}
|
||||
topic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
onComponentUpdate={messagesComponentUpdateHandler}
|
||||
onFirstUpdate={messagesComponentFirstUpdateHandler}
|
||||
/>
|
||||
<ContentSearch
|
||||
ref={contentSearchRef}
|
||||
searchTarget={mainRef as React.RefObject<HTMLElement>}
|
||||
filter={contentSearchFilter}
|
||||
includeUser={filterIncludeUser}
|
||||
onIncludeUserChange={userOutlinedItemClickHandler}
|
||||
/>
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</QuickPanelProvider>
|
||||
|
||||
@@ -71,6 +71,7 @@ interface Props {
|
||||
|
||||
let _text = ''
|
||||
let _files: FileType[] = []
|
||||
let _mentionedModelsCache: Model[] = []
|
||||
|
||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
@@ -103,7 +104,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||
const [mentionedModels, setMentionedModels] = useState<Model[]>([])
|
||||
const [mentionedModels, setMentionedModels] = useState<Model[]>(_mentionedModelsCache)
|
||||
const mentionedModelsRef = useRef(mentionedModels)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isFileDragging, setIsFileDragging] = useState(false)
|
||||
const [textareaHeight, setTextareaHeight] = useState<number>()
|
||||
@@ -114,6 +116,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model])
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
useEffect(() => {
|
||||
mentionedModelsRef.current = mentionedModels
|
||||
}, [mentionedModels])
|
||||
|
||||
const isVisionSupported = useMemo(
|
||||
() =>
|
||||
(mentionedModels.length > 0 && isVisionModels(mentionedModels)) ||
|
||||
@@ -179,6 +185,13 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
_text = text
|
||||
_files = files
|
||||
|
||||
useEffect(() => {
|
||||
// 利用useEffect清理函数在卸载组件时更新状态缓存
|
||||
return () => {
|
||||
_mentionedModelsCache = mentionedModelsRef.current
|
||||
}
|
||||
}, [])
|
||||
|
||||
const focusTextarea = useCallback(() => {
|
||||
textareaRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
@@ -8,7 +8,13 @@ import {
|
||||
MdiLightbulbOn80
|
||||
} from '@renderer/components/Icons/SVGIcon'
|
||||
import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { getThinkModelType, isDoubaoThinkingAutoModel, MODEL_SUPPORTED_OPTIONS } from '@renderer/config/models'
|
||||
import {
|
||||
getThinkModelType,
|
||||
isDoubaoThinkingAutoModel,
|
||||
isGPT5SeriesReasoningModel,
|
||||
isOpenAIWebSearchModel,
|
||||
MODEL_SUPPORTED_OPTIONS
|
||||
} from '@renderer/config/models'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { getReasoningEffortOptionsLabel } from '@renderer/i18n/label'
|
||||
import { Model, ThinkingOption } from '@renderer/types'
|
||||
@@ -61,6 +67,15 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistantId }): ReactElement =>
|
||||
})
|
||||
return
|
||||
}
|
||||
if (
|
||||
isOpenAIWebSearchModel(model) &&
|
||||
isGPT5SeriesReasoningModel(model) &&
|
||||
assistant.enableWebSearch &&
|
||||
option === 'minimal'
|
||||
) {
|
||||
window.toast.warning(t('chat.web_search.warning.openai'))
|
||||
return
|
||||
}
|
||||
updateAssistantSettings({
|
||||
reasoning_effort: option,
|
||||
reasoning_effort_cache: option,
|
||||
@@ -68,7 +83,7 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistantId }): ReactElement =>
|
||||
})
|
||||
return
|
||||
},
|
||||
[updateAssistantSettings]
|
||||
[updateAssistantSettings, assistant.enableWebSearch, model, t]
|
||||
)
|
||||
|
||||
const panelItems = useMemo(() => {
|
||||
|
||||
@@ -3,7 +3,12 @@ import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons'
|
||||
import { QuickPanelListItem, QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { isGeminiModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import {
|
||||
isGeminiModel,
|
||||
isGPT5SeriesReasoningModel,
|
||||
isOpenAIWebSearchModel,
|
||||
isWebSearchModel
|
||||
} from '@renderer/config/models'
|
||||
import { isGeminiWebSearchProvider } from '@renderer/config/providers'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
@@ -115,6 +120,15 @@ const WebSearchButton: FC<Props> = ({ ref, assistantId }) => {
|
||||
update.enableWebSearch = false
|
||||
window.toast.warning(t('chat.mcp.warning.gemini_web_search'))
|
||||
}
|
||||
if (
|
||||
isOpenAIWebSearchModel(model) &&
|
||||
isGPT5SeriesReasoningModel(model) &&
|
||||
update.enableWebSearch &&
|
||||
assistant.settings?.reasoning_effort === 'minimal'
|
||||
) {
|
||||
update.enableWebSearch = false
|
||||
window.toast.warning(t('chat.web_search.warning.openai'))
|
||||
}
|
||||
setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200)
|
||||
}, [assistant, setTimeoutTimer, t, updateAssistant])
|
||||
|
||||
|
||||
@@ -16,7 +16,13 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { updateWebSearchProvider } from '@renderer/store/websearch'
|
||||
import { isSystemProvider } from '@renderer/types'
|
||||
import { ApiKeyConnectivity, HealthStatus } from '@renderer/types/healthCheck'
|
||||
import { formatApiHost, formatApiKeys, getFancyProviderName, isOpenAIProvider } from '@renderer/utils'
|
||||
import {
|
||||
formatApiHost,
|
||||
formatApiKeys,
|
||||
getFancyProviderName,
|
||||
isAnthropicProvider,
|
||||
isOpenAIProvider
|
||||
} from '@renderer/utils'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
@@ -212,6 +218,10 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
if (provider.type === 'azure-openai') {
|
||||
return formatApiHost(apiHost) + 'openai/v1'
|
||||
}
|
||||
|
||||
if (provider.type === 'anthropic') {
|
||||
return formatApiHost(apiHost) + 'messages'
|
||||
}
|
||||
return formatApiHost(apiHost) + 'responses'
|
||||
}
|
||||
|
||||
@@ -361,7 +371,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
{(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
|
||||
@@ -117,7 +117,8 @@ export async function fetchChatCompletion({
|
||||
const {
|
||||
params: aiSdkParams,
|
||||
modelId,
|
||||
capabilities
|
||||
capabilities,
|
||||
webSearchPluginConfig
|
||||
} = await buildStreamTextParams(messages, assistant, provider, {
|
||||
mcpTools: mcpTools,
|
||||
webSearchProviderId: assistant.webSearchProviderId,
|
||||
@@ -132,6 +133,7 @@ export async function fetchChatCompletion({
|
||||
isPromptToolUse: isPromptToolUse(assistant),
|
||||
isSupportedToolUse: isSupportedToolUse(assistant),
|
||||
isImageGenerationEndpoint: isDedicatedImageGenerationModel(assistant.model || getDefaultModel()),
|
||||
webSearchPluginConfig: webSearchPluginConfig,
|
||||
enableWebSearch: capabilities.enableWebSearch,
|
||||
enableGenerateImage: capabilities.enableGenerateImage,
|
||||
enableUrlContext: capabilities.enableUrlContext,
|
||||
|
||||
@@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 155,
|
||||
version: 156,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -2485,6 +2485,21 @@ const migrateConfig = {
|
||||
logger.error('migrate 155 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'156': (state: RootState) => {
|
||||
try {
|
||||
state.llm.providers.forEach((provider) => {
|
||||
if (provider.id === SystemProviderIds.anthropic) {
|
||||
if (provider.apiHost.endsWith('/')) {
|
||||
provider.apiHost = provider.apiHost.slice(0, -1)
|
||||
}
|
||||
}
|
||||
})
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 156 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface WebSearchState {
|
||||
providerConfig: Record<string, any>
|
||||
}
|
||||
|
||||
export type CherryWebSearchConfig = Pick<WebSearchState, 'searchWithTime' | 'maxResults' | 'excludeDomains'>
|
||||
|
||||
export const initialState: WebSearchState = {
|
||||
defaultProvider: 'local-bing',
|
||||
providers: WEB_SEARCH_PROVIDERS,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { MatchPatternMap } from '../blacklistMatchPattern'
|
||||
import { mapRegexToPatterns, MatchPatternMap } from '../blacklistMatchPattern'
|
||||
|
||||
function get(map: MatchPatternMap<number>, url: string) {
|
||||
return map.get(url).sort()
|
||||
@@ -161,3 +161,28 @@ describe('blacklistMatchPattern', () => {
|
||||
expect(get(map, 'https://b.mozilla.org/path/')).toEqual([0, 1, 2, 6])
|
||||
})
|
||||
})
|
||||
|
||||
describe('mapRegexToPatterns', () => {
|
||||
it('extracts domains from regex patterns', () => {
|
||||
const result = mapRegexToPatterns([
|
||||
'/example\\.com/',
|
||||
'/(?:www\\.)?sub\\.example\\.co\\.uk/',
|
||||
'/api\\.service\\.io/',
|
||||
'https://baidu.com'
|
||||
])
|
||||
|
||||
expect(result).toEqual(['example.com', 'sub.example.co.uk', 'api.service.io', 'baidu.com'])
|
||||
})
|
||||
|
||||
it('deduplicates domains across multiple patterns', () => {
|
||||
const result = mapRegexToPatterns(['/example\\.com/', '/(example\\.com|test\\.org)/'])
|
||||
|
||||
expect(result).toEqual(['example.com', 'test.org'])
|
||||
})
|
||||
|
||||
it('ignores patterns without domain matches', () => {
|
||||
const result = mapRegexToPatterns(['', 'plain-domain.com', '/^https?:\\/\\/[^/]+$/'])
|
||||
|
||||
expect(result).toEqual(['plain-domain.com'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -202,6 +202,43 @@ export async function parseSubscribeContent(url: string): Promise<string[]> {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function mapRegexToPatterns(patterns: string[]): string[] {
|
||||
const patternSet = new Set<string>()
|
||||
const domainMatcher = /[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g
|
||||
|
||||
patterns.forEach((pattern) => {
|
||||
if (!pattern) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle regex patterns (wrapped in /)
|
||||
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
||||
const rawPattern = pattern.slice(1, -1)
|
||||
const normalizedPattern = rawPattern.replace(/\\\./g, '.').replace(/\\\//g, '/')
|
||||
const matches = normalizedPattern.match(domainMatcher)
|
||||
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
patternSet.add(match.replace(/http(s)?:\/\//g, '').toLowerCase())
|
||||
})
|
||||
}
|
||||
} else if (pattern.includes('://')) {
|
||||
// Handle URLs with protocol (e.g., https://baidu.com)
|
||||
const matches = pattern.match(domainMatcher)
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
patternSet.add(match.replace(/http(s)?:\/\//g, '').toLowerCase())
|
||||
})
|
||||
}
|
||||
} else {
|
||||
patternSet.add(pattern.toLowerCase())
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(patternSet)
|
||||
}
|
||||
|
||||
export async function filterResultWithBlacklist(
|
||||
response: WebSearchProviderResponse,
|
||||
websearch: WebSearchState
|
||||
|
||||
@@ -205,6 +205,10 @@ export function isOpenAIProvider(provider: Provider): boolean {
|
||||
return !['anthropic', 'gemini', 'vertexai'].includes(provider.type)
|
||||
}
|
||||
|
||||
export function isAnthropicProvider(provider: Provider): boolean {
|
||||
return provider.type === 'anthropic'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断模型是否为用户手动选择
|
||||
* @param {Model} model 模型对象
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import '@renderer/databases'
|
||||
|
||||
import { HeroUIProvider } from '@heroui/react'
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { ToastPortal } from '@renderer/components/ToastPortal'
|
||||
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
||||
import { HeroUIProvider } from '@renderer/context/HeroUIProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import store, { persistor } from '@renderer/store'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
@@ -2,13 +2,13 @@ import '@renderer/assets/styles/index.css'
|
||||
import '@renderer/assets/styles/tailwind.css'
|
||||
import '@ant-design/v5-patch-for-react-19'
|
||||
|
||||
import { HeroUIProvider } from '@heroui/react'
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import { loggerService } from '@logger'
|
||||
import { ToastPortal } from '@renderer/components/ToastPortal'
|
||||
import { getToastUtilities } from '@renderer/components/TopView/toast'
|
||||
import AntdProvider from '@renderer/context/AntdProvider'
|
||||
import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider'
|
||||
import { HeroUIProvider } from '@renderer/context/HeroUIProvider'
|
||||
import { ThemeProvider } from '@renderer/context/ThemeProvider'
|
||||
import storeSyncService from '@renderer/services/StoreSyncService'
|
||||
import store, { persistor } from '@renderer/store'
|
||||
|
||||
Reference in New Issue
Block a user