diff --git a/README.md b/README.md index c7b87c61..fafceb86 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,13 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai ### Install ```bash -$ yarn +yarn ``` ### Development ```bash -$ yarn dev +yarn dev ``` ### Build @@ -135,6 +135,8 @@ Thank you for your support and contributions! - [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution. +- [ublacklist](https://github.com/iorate/ublacklist):Blocks specific sites from appearing in Google search results + # 🚀 Contributors diff --git a/package.json b/package.json index 3623a8cb..740aec3f 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,12 @@ "generate:icons": "electron-icon-builder --input=./build/logo.png --output=build", "analyze:renderer": "VISUALIZER_RENDERER=true yarn build", "analyze:main": "VISUALIZER_MAIN=true yarn build", - "check": "node scripts/check-i18n.js" + "check": "node scripts/check-i18n.js", + "test": "tsx --test src/**/*.test.ts" }, "dependencies": { + "@agentic/searxng": "^7.3.3", + "@agentic/tavily": "^7.3.3", "@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/utils": "^3.0.0", "@electron/notarize": "^2.5.0", diff --git a/src/renderer/src/assets/images/search/searxng.svg b/src/renderer/src/assets/images/search/searxng.svg new file mode 100755 index 00000000..f3ff8e33 --- /dev/null +++ b/src/renderer/src/assets/images/search/searxng.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/assets/images/search/tavily.png b/src/renderer/src/assets/images/search/tavily.png new file mode 100644 index 00000000..f1dccb0f Binary files /dev/null and b/src/renderer/src/assets/images/search/tavily.png differ diff --git a/src/renderer/src/config/webSearchProviders.ts b/src/renderer/src/config/webSearchProviders.ts new file mode 100644 index 00000000..f65a0045 --- /dev/null +++ b/src/renderer/src/config/webSearchProviders.ts @@ -0,0 +1,30 @@ +import SearxngLogo from '@renderer/assets/images/search/searxng.svg' +import TavilyLogo from '@renderer/assets/images/search/tavily.png' +import TavilyLogoDark from '@renderer/assets/images/search/tavily-dark.svg' +export function getWebSearchProviderLogo(providerId: string) { + switch (providerId) { + case 'tavily': + return TavilyLogo + case 'tavily-dark': + return TavilyLogoDark + case 'searxng': + return SearxngLogo + + default: + return undefined + } +} + +export const WEB_SEARCH_PROVIDER_CONFIG = { + tavily: { + websites: { + official: 'https://tavily.com', + apiKey: 'https://app.tavily.com/home' + } + }, + searxng: { + websites: { + official: 'https://docs.searxng.org' + } + } +} diff --git a/src/renderer/src/hooks/useWebSearchProviders.ts b/src/renderer/src/hooks/useWebSearchProviders.ts index 7c61e5bb..e2f0673f 100644 --- a/src/renderer/src/hooks/useWebSearchProviders.ts +++ b/src/renderer/src/hooks/useWebSearchProviders.ts @@ -1,17 +1,17 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setDefaultProvider as _setDefaultProvider, updateWebSearchProvider } from '@renderer/store/websearch' +import { + setDefaultProvider as _setDefaultProvider, + updateWebSearchProvider, + updateWebSearchProviders +} from '@renderer/store/websearch' import { WebSearchProvider } from '@renderer/types' export const useDefaultWebSearchProvider = () => { const defaultProvider = useAppSelector((state) => state.websearch.defaultProvider) - const providers = useWebSearchProviders() - const provider = providers.find((provider) => provider.id === defaultProvider) + const { providers } = useWebSearchProviders() + const provider = defaultProvider ? providers.find((provider) => provider.id === defaultProvider) : undefined const dispatch = useAppDispatch() - if (!provider) { - throw new Error(`Web search provider with id ${defaultProvider} not found`) - } - const setDefaultProvider = (provider: WebSearchProvider) => { dispatch(_setDefaultProvider(provider.id)) } @@ -25,14 +25,18 @@ export const useDefaultWebSearchProvider = () => { export const useWebSearchProviders = () => { const providers = useAppSelector((state) => state.websearch.providers) - return providers + const dispatch = useAppDispatch() + + return { + providers, + updateWebSearchProviders: (providers: WebSearchProvider[]) => dispatch(updateWebSearchProviders(providers)) + } } export const useWebSearchProvider = (id: string) => { const providers = useAppSelector((state) => state.websearch.providers) const provider = providers.find((provider) => provider.id === id) const dispatch = useAppDispatch() - if (!provider) { throw new Error(`Web search provider with id ${id} not found`) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 6a804af9..b0a7bada 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -104,7 +104,7 @@ "input.web_search": "Enable web search", "input.web_search.button.ok": "Go to Settings", "input.web_search.enable": "Enable web search", - "input.web_search.enable_content": "Enable web search in Settings", + "input.web_search.enable_content": "Need to check web search connectivity in settings first", "input.auto_resize": "Auto resize height", "message.new.branch": "New Branch", "message.new.branch.created": "New Branch Created", @@ -863,7 +863,12 @@ "blacklist_description": "Results from the following websites will not appear in search results", "blacklist_tooltip": "Please use the following format (separated by line breaks)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "search_max_result": "Number of search results", - "search_result_default": "Default" + "search_result_default": "Default", + "check": "Check", + "search_provider": "Search service provider", + "search_provider_placeholder": "Choose a search service provider.", + "no_provider_selected": "Please select a search service provider before checking.", + "check_failed": "Verification failed" }, "mcp": { "title": "MCP Servers", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index ee4c609d..b36eaeec 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -104,7 +104,7 @@ "input.web_search": "ウェブ検索を有効にする", "input.web_search.button.ok": "設定に移動", "input.web_search.enable": "ウェブ検索を有効にする", - "input.web_search.enable_content": "ウェブ検索を有効にするには、設定でウェブ検索を有効にする必要があります", + "input.web_search.enable_content": "ウェブ検索の接続性を先に設定で確認する必要があります", "input.auto_resize": "高さを自動調整", "message.new.branch": "新しいブランチ", "message.new.branch.created": "新しいブランチが作成されました", @@ -863,7 +863,12 @@ "blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません", "blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "search_max_result": "検索結果の数", - "search_result_default": "デフォルト" + "search_result_default": "デフォルト", + "check": "チェック", + "search_provider": "検索サービスプロバイダー", + "search_provider_placeholder": "検索サービスプロバイダーを選択する", + "no_provider_selected": "検索サービスプロバイダーを選択してから再確認してください。", + "check_failed": "検証に失敗しました" }, "mcp": { "title": "MCP サーバー", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 37585067..73c075a3 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -104,7 +104,7 @@ "input.web_search": "Включить веб-поиск", "input.web_search.button.ok": "Перейти в Настройки", "input.web_search.enable": "Включить веб-поиск", - "input.web_search.enable_content": "Необходимо включить веб-поиск в Настройки", + "input.web_search.enable_content": "Необходимо предварительно проверить подключение к веб-поиску в настройках", "input.auto_resize": "Автоматическая высота", "message.new.branch": "Новая ветка", "message.new.branch.created": "Новая ветка создана", @@ -863,7 +863,12 @@ "blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска", "blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "search_max_result": "Количество результатов поиска", - "search_result_default": "По умолчанию" + "search_result_default": "По умолчанию", + "check": "проверка", + "search_provider": "поиск сервисного провайдера", + "search_provider_placeholder": "Выберите поставщика поисковых услуг", + "no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.", + "check_failed": "Проверка не прошла" }, "mcp": { "title": "Серверы MCP", @@ -964,3 +969,4 @@ } } } + diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 061d91c8..d8f14193 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -104,7 +104,7 @@ "input.web_search": "开启网络搜索", "input.web_search.button.ok": "去设置", "input.web_search.enable": "开启网络搜索", - "input.web_search.enable_content": "需要先在设置中开启网络搜索", + "input.web_search.enable_content": "需要先在设置中检查网络搜索连通性", "input.auto_resize": "自动调整高度", "message.new.branch": "分支", "message.new.branch.created": "新分支已创建", @@ -850,6 +850,8 @@ "assistant.show.icon": "显示模型图标", "tray.title": "启用系统托盘图标", "websearch": { + "check": "检查", + "check_failed": "验证失败", "blacklist": "黑名单", "blacklist_description": "在搜索结果中不会出现以下网站的结果", "blacklist_tooltip": "请使用以下格式(换行分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", @@ -857,6 +859,9 @@ "search_max_result": "搜索结果个数", "search_result_default": "默认", "search_with_time": "搜索包含日期", + "search_provider": "搜索服务商", + "search_provider_placeholder": "选择一个搜索服务商", + "no_provider_selected": "请选择搜索服务商后再检查", "tavily": { "api_key": "Tavily API 密钥", "api_key.placeholder": "请输入 Tavily API 密钥", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 7a8f5866..5cda5203 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -859,10 +859,15 @@ }, "title": "網路搜尋", "blacklist": "黑名單", - "blacklist_description": "以下網站不會出現在搜尋結果中", - "blacklist_tooltip": "請使用以下格式 (換行分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", + "blacklist_description": "以下網站不會出現在搜索結果中", + "blacklist_tooltip": "請使用以下格式(換行分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "search_max_result": "搜尋結果個數", - "search_result_default": "預設" + "search_result_default": "預設", + "check": "檢查", + "search_provider": "搜尋服務商", + "search_provider_placeholder": "選擇一個搜尋服務商", + "no_provider_selected": "請選擇搜索服務商後再檢查", + "check_failed": "驗證失敗" }, "display.assistant.title": "助手設定", "mcp": { diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx index 908ab06a..ba342600 100644 --- a/src/renderer/src/pages/home/Messages/MessageContent.tsx +++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx @@ -29,13 +29,13 @@ const MessageContent: React.FC = ({ message: _message, model }) => { // Process content to make citation numbers clickable const processedContent = useMemo(() => { - if (!(message.metadata?.citations || message.metadata?.tavily)) { + if (!(message.metadata?.citations || message.metadata?.webSearch)) { return message.content } let content = message.content - const searchResultsCitations = message?.metadata?.tavily?.results?.map((result) => result.url) || [] + const searchResultsCitations = message?.metadata?.webSearch?.results?.map((result) => result.url) || [] const citations = message?.metadata?.citations || searchResultsCitations @@ -127,13 +127,13 @@ const MessageContent: React.FC = ({ message: _message, model }) => { ))} )} - {message?.metadata?.tavily && message.status === 'success' && ( + {message?.metadata?.webSearch && message.status === 'success' && ( {t('message.citations')} - {message.metadata.tavily.results.map((result, index) => ( + {message.metadata.webSearch.results.map((result, index) => ( {index + 1}. diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.tsx b/src/renderer/src/pages/settings/ProviderSettings/index.tsx index c26f2dbd..270bdb6c 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/index.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/index.tsx @@ -268,5 +268,4 @@ const AddButtonWrapper = styled.div` align-items: center; padding: 10px 8px; ` - export default ProvidersList diff --git a/src/renderer/src/pages/settings/WebSearchSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings.tsx deleted file mode 100644 index ddb3541c..00000000 --- a/src/renderer/src/pages/settings/WebSearchSettings.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import tavilyLogo from '@renderer/assets/images/search/tavily.svg' -import tavilyLogoDark from '@renderer/assets/images/search/tavily-dark.svg' -import { HStack } from '@renderer/components/Layout' -import { useTheme } from '@renderer/context/ThemeProvider' -import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' -import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setExcludeDomains, setMaxResult, setSearchWithTime } from '@renderer/store/websearch' -import { formatDomains } from '@renderer/utils/blacklist' -import { Alert, Button, Input, Slider, Switch, Typography } from 'antd' -import TextArea from 'antd/es/input/TextArea' -import { FC, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -import { - SettingContainer, - SettingDivider, - SettingGroup, - SettingHelpLink, - SettingHelpTextRow, - SettingRow, - SettingRowTitle, - SettingTitle -} from '.' - -const WebSearchSettings: FC = () => { - const { t } = useTranslation() - const { Paragraph } = Typography - const { theme } = useTheme() - const { provider, updateProvider } = useWebSearchProvider('tavily') - const [apiKey, setApiKey] = useState(provider.apiKey) - const logo = theme === 'dark' ? tavilyLogoDark : tavilyLogo - const searchWithTime = useAppSelector((state) => state.websearch.searchWithTime) - const maxResults = useAppSelector((state) => state.websearch.maxResults) - const excludeDomains = useAppSelector((state) => state.websearch.excludeDomains) - const [errFormat, setErrFormat] = useState(false) - const [blacklistInput, setBlacklistInput] = useState('') - - const dispatch = useAppDispatch() - - useEffect(() => { - return () => { - if (apiKey && apiKey !== provider.apiKey) { - updateProvider({ ...provider, apiKey }) - } - } - }, [apiKey, provider, updateProvider]) - - useEffect(() => { - if (excludeDomains) { - setBlacklistInput(excludeDomains.join('\n')) - } - }, [excludeDomains]) - - function updateManualBlacklist(blacklist: string) { - const blacklistDomains = blacklist.split('\n').filter((url) => url.trim() !== '') - const { formattedDomains, hasError } = formatDomains(blacklistDomains) - setErrFormat(hasError) - if (hasError) return - dispatch(setExcludeDomains(formattedDomains)) - } - - return ( - - - - - - - - {t('settings.websearch.tavily.description')} - - setApiKey(e.target.value)} - onBlur={() => updateProvider({ ...provider, apiKey })} - /> - - - {t('settings.websearch.get_api_key')} - - - - - {t('settings.general.title')} - - - {t('settings.websearch.search_with_time')} - dispatch(setSearchWithTime(checked))} /> - - - - {t('settings.websearch.search_max_result')} - dispatch(setMaxResult(value))} - /> - - - - {t('settings.websearch.blacklist')} - - - {t('settings.websearch.blacklist_description')} - -