Merge remote-tracking branch 'origin/main' into v2

This commit is contained in:
MyPrototypeWhat
2025-09-17 18:37:22 +08:00
39 changed files with 424 additions and 186 deletions
@@ -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'
@@ -27,6 +28,8 @@ export interface AiSdkMiddlewareConfig {
enableUrlContext: boolean
mcpTools?: MCPTool[]
uiMessages?: Message[]
// 内置搜索配置
webSearchPluginConfig?: WebSearchPluginConfig
}
/**
@@ -30,9 +30,8 @@ export async 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
}
}
+81 -7
View File
@@ -1,6 +1,13 @@
import type {
AnthropicSearchConfig,
OpenAISearchConfig,
WebSearchPluginConfig
} from '@cherrystudio/ai-core/core/plugins/built-in/webSearchPlugin/helper'
import type { BaseProviderId } from '@cherrystudio/ai-core/provider'
import { isOpenAIWebSearchChatCompletionOnlyModel } from '@renderer/config/models'
import type { CherryWebSearchConfig } from '@renderer/store/websearch'
import type { Model } from '@renderer/types'
import { WEB_SEARCH_PROMPT_FOR_OPENROUTER } from '@shared/config/prompts'
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}`)
}
}
}
+5
View File
@@ -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')
+1 -1
View File
@@ -1013,7 +1013,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/',
+6 -1
View File
@@ -660,7 +660,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",
+6 -2
View File
@@ -132,7 +132,6 @@
},
"title": "API 服务器"
},
"assistants": {
"abbr": "助手",
"clear": {
@@ -661,7 +660,12 @@
"title": "话题",
"unpin": "取消固定"
},
"translate": "翻译"
"translate": "翻译",
"web_search": {
"warning": {
"openai": "GPT5 模型 minimal 思考强度不支持网络搜索"
}
}
},
"code": {
"auto_update_to_latest": "检查更新并安装最新版本",
+6 -1
View File
@@ -660,7 +660,12 @@
"title": "話題",
"unpin": "取消固定"
},
"translate": "翻譯"
"translate": "翻譯",
"web_search": {
"warning": {
"openai": "GPT-5 模型的最小推理力度不支援網路搜尋。"
}
}
},
"code": {
"auto_update_to_latest": "檢查更新並安裝最新版本",
+8 -2
View File
@@ -362,8 +362,9 @@
"translate": "Μετάφραση στο {{target_language}}",
"translating": "Μετάφραση...",
"upload": {
"attachment": "Μεταφόρτωση συνημμένου",
"document": "Φόρτωση έγγραφου (το μοντέλο δεν υποστηρίζει εικόνες)",
"label": "Φόρτωση εικόνας ή έγγραφου",
"image_or_document": "Μεταφόρτωση εικόνας ή εγγράφου",
"upload_from_local": "Μεταφόρτωση αρχείου από τον υπολογιστή..."
},
"url_context": "Περιεχόμενο ιστοσελίδας",
@@ -659,7 +660,12 @@
"title": "Θέματα",
"unpin": "Ξεκαρφίτσωμα"
},
"translate": "Μετάφραση"
"translate": "Μετάφραση",
"web_search": {
"warning": {
"openai": "Το μοντέλο GPT5 με ελάχιστη ένταση σκέψης δεν υποστηρίζει αναζήτηση στο διαδίκτυο"
}
}
},
"code": {
"auto_update_to_latest": "Έλεγχος για ενημερώσεις και εγκατάσταση της τελευταίας έκδοσης",
+8 -2
View File
@@ -362,8 +362,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",
@@ -659,7 +660,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",
+8 -2
View File
@@ -362,8 +362,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",
@@ -659,7 +660,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",
+8 -2
View File
@@ -362,8 +362,9 @@
"translate": "{{target_language}}に翻訳",
"translating": "翻訳中...",
"upload": {
"attachment": "添付ファイルをアップロード",
"document": "ドキュメントをアップロード(モデルは画像をサポートしません)",
"label": "画像またはドキュメントをアップロード",
"image_or_document": "画像またはドキュメントをアップロード",
"upload_from_local": "ローカルファイルをアップロード..."
},
"url_context": "URLコンテキスト",
@@ -659,7 +660,12 @@
"title": "トピック",
"unpin": "固定解除"
},
"translate": "翻訳"
"translate": "翻訳",
"web_search": {
"warning": {
"openai": "GPT5モデルの最小思考強度ではネット検索はサポートされません"
}
}
},
"code": {
"auto_update_to_latest": "最新バージョンを自動的に更新する",
+8 -2
View File
@@ -362,8 +362,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",
@@ -659,7 +660,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",
+8 -2
View File
@@ -362,8 +362,9 @@
"translate": "Перевести на {{target_language}}",
"translating": "Перевод...",
"upload": {
"attachment": "Загрузить вложение",
"document": "Загрузить документ (модель не поддерживает изображения)",
"label": "Загрузить изображение или документ",
"image_or_document": "Загрузить изображение или документ",
"upload_from_local": "Загрузить локальный файл..."
},
"url_context": "Контекст страницы",
@@ -659,7 +660,12 @@
"title": "Топики",
"unpin": "Открепленные темы"
},
"translate": "Перевести"
"translate": "Перевести",
"web_search": {
"warning": {
"openai": "Модель GPT5 с минимальной интенсивностью мышления не поддерживает поиск в интернете"
}
}
},
"code": {
"auto_update_to_latest": "Автоматически обновлять до последней версии",
@@ -74,6 +74,7 @@ interface Props {
let _text = ''
let _files: FileType[] = []
let _mentionedModelsCache: Model[] = []
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
const [targetLanguage] = usePreference('feature.translate.target_language')
@@ -106,7 +107,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>()
@@ -117,6 +119,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)) ||
@@ -182,6 +188,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 type { Model, ThinkingOption } from '@renderer/types'
@@ -62,6 +68,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,
@@ -69,7 +84,7 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistantId }): ReactElement =>
})
return
},
[updateAssistantSettings]
[updateAssistantSettings, assistant.enableWebSearch, model, t]
)
const panelItems = useMemo(() => {
@@ -4,7 +4,12 @@ import { ActionIconButton } from '@renderer/components/Buttons'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { 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'
@@ -117,6 +122,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])
@@ -18,7 +18,13 @@ import { updateWebSearchProvider } from '@renderer/store/websearch'
import { isSystemProvider } from '@renderer/types'
import type { ApiKeyConnectivity } from '@renderer/types/healthCheck'
import { 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, Input, Select, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
@@ -215,6 +221,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'
}
@@ -364,7 +374,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' }}>
+3 -1
View File
@@ -118,7 +118,8 @@ export async function fetchChatCompletion({
const {
params: aiSdkParams,
modelId,
capabilities
capabilities,
webSearchPluginConfig
} = await buildStreamTextParams(messages, assistant, provider, {
mcpTools: mcpTools,
webSearchProviderId: assistant.webSearchProviderId,
@@ -133,6 +134,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,
+1 -1
View File
@@ -71,7 +71,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 155,
version: 156,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},
+15
View File
@@ -2480,6 +2480,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
}
}
}
+2
View File
@@ -42,6 +42,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
+4
View File
@@ -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