feat(websearch): enhance web search provider settings and localization

- Updated web search provider settings to include API key and free status indicators.
- Improved localization for English, Japanese, Russian, Chinese, and Taiwanese languages to reflect new API key and free status fields.
- Refactored web search provider management to prevent duplicates and streamline provider addition during state migration.
- Adjusted UI components to conditionally render based on provider type, enhancing user experience.
This commit is contained in:
kangfenmao 2025-04-10 13:07:55 +08:00
parent f9c6bddae5
commit efcffbaa30
12 changed files with 807 additions and 764 deletions

View File

@ -25,6 +25,7 @@ export const useDefaultWebSearchProvider = () => {
export const useWebSearchProviders = () => { export const useWebSearchProviders = () => {
const providers = useAppSelector((state) => state.websearch.providers) const providers = useAppSelector((state) => state.websearch.providers)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return { return {
@ -45,6 +46,7 @@ export const useWebSearchProvider = (id: string) => {
const providers = useAppSelector((state) => state.websearch.providers) const providers = useAppSelector((state) => state.websearch.providers)
const provider = providers.find((provider) => provider.id === id) const provider = providers.find((provider) => provider.id === id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
if (!provider) { if (!provider) {
throw new Error(`Web search provider with id ${id} not found`) throw new Error(`Web search provider with id ${id} not found`)
} }

View File

@ -1301,7 +1301,9 @@
}, },
"title": "Web Search", "title": "Web Search",
"overwrite": "Override search service", "overwrite": "Override search service",
"overwrite_tooltip": "Force use search service instead of LLM" "overwrite_tooltip": "Force use search service instead of LLM",
"apikey": "API key",
"free": "Free"
}, },
"quickPhrase": { "quickPhrase": {
"title": "Quick Phrases", "title": "Quick Phrases",

View File

@ -1300,7 +1300,9 @@
}, },
"title": "ウェブ検索", "title": "ウェブ検索",
"overwrite": "サービス検索を上書き", "overwrite": "サービス検索を上書き",
"overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する" "overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する",
"apikey": "API キー",
"free": "無料"
}, },
"general.auto_check_update.title": "自動更新チェックを有効にする", "general.auto_check_update.title": "自動更新チェックを有効にする",
"quickPhrase": { "quickPhrase": {

View File

@ -1300,7 +1300,9 @@
}, },
"title": "Поиск в Интернете", "title": "Поиск в Интернете",
"overwrite": "Переопределить поставщика поиска", "overwrite": "Переопределить поставщика поиска",
"overwrite_tooltip": "Использовать поставщика поиска вместо LLM" "overwrite_tooltip": "Использовать поставщика поиска вместо LLM",
"apikey": "Ключ API",
"free": "Бесплатно"
}, },
"general.auto_check_update.title": "Включить автоматическую проверку обновлений", "general.auto_check_update.title": "Включить автоматическую проверку обновлений",
"quickPhrase": { "quickPhrase": {

View File

@ -1301,7 +1301,9 @@
"description": "Tavily 是一个为 AI 代理量身定制的搜索引擎,提供实时、准确的结果、智能查询建议和深入的研究能力", "description": "Tavily 是一个为 AI 代理量身定制的搜索引擎,提供实时、准确的结果、智能查询建议和深入的研究能力",
"title": "Tavily" "title": "Tavily"
}, },
"title": "网络搜索" "title": "网络搜索",
"apikey": "API 密钥",
"free": "免费"
}, },
"quickPhrase": { "quickPhrase": {
"title": "快捷短语", "title": "快捷短语",

View File

@ -1300,7 +1300,9 @@
}, },
"title": "網路搜尋", "title": "網路搜尋",
"overwrite": "覆蓋搜尋服務商", "overwrite": "覆蓋搜尋服務商",
"overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋" "overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋",
"apikey": "API 金鑰",
"free": "免費"
}, },
"general.auto_check_update.title": "啟用自動更新檢查", "general.auto_check_update.title": "啟用自動更新檢查",
"quickPhrase": { "quickPhrase": {

View File

@ -117,7 +117,6 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
<SettingTitle> <SettingTitle>
<Flex align="center" gap={8}> <Flex align="center" gap={8}>
<ProviderLogo shape="square" src={getWebSearchProviderLogo(provider.id)} size={16} /> <ProviderLogo shape="square" src={getWebSearchProviderLogo(provider.id)} size={16} />
<ProviderName> {provider.name}</ProviderName> <ProviderName> {provider.name}</ProviderName>
{officialWebsite && webSearchProviderConfig?.websites && ( {officialWebsite && webSearchProviderConfig?.websites && (
<Link target="_blank" href={webSearchProviderConfig.websites.official}> <Link target="_blank" href={webSearchProviderConfig.websites.official}>
@ -156,7 +155,6 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
</SettingHelpTextRow> </SettingHelpTextRow>
</> </>
)} )}
{hasObjectKey(provider, 'apiHost') && ( {hasObjectKey(provider, 'apiHost') && (
<> <>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}> <SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>

View File

@ -1,10 +1,9 @@
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders' import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import { defaultWebSearchProviders } from '@renderer/store/websearch'
import { WebSearchProvider } from '@renderer/types' import { WebSearchProvider } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils' import { hasObjectKey } from '@renderer/utils'
import { Select } from 'antd' import { Select } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
@ -13,15 +12,13 @@ import BlacklistSettings from './BlacklistSettings'
import WebSearchProviderSetting from './WebSearchProviderSetting' import WebSearchProviderSetting from './WebSearchProviderSetting'
const WebSearchSettings: FC = () => { const WebSearchSettings: FC = () => {
const { providers, addWebSearchProvider } = useWebSearchProviders() const { providers } = useWebSearchProviders()
const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider() const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider()
const { t } = useTranslation() const { t } = useTranslation()
const [selectedProvider, setSelectedProvider] = useState<WebSearchProvider | undefined>(defaultProvider) const [selectedProvider, setSelectedProvider] = useState<WebSearchProvider | undefined>(defaultProvider)
const { theme: themeMode } = useTheme() const { theme: themeMode } = useTheme()
useEffect(() => { const isLocalProvider = selectedProvider?.id.startsWith('local')
defaultWebSearchProviders.map((p) => addWebSearchProvider(p))
})
function updateSelectedWebSearchProvider(providerId: string) { function updateSelectedWebSearchProvider(providerId: string) {
const provider = providers.find((p) => p.id === providerId) const provider = providers.find((p) => p.id === providerId)
@ -45,19 +42,19 @@ const WebSearchSettings: FC = () => {
style={{ width: '200px' }} style={{ width: '200px' }}
onChange={(value: string) => updateSelectedWebSearchProvider(value)} onChange={(value: string) => updateSelectedWebSearchProvider(value)}
placeholder={t('settings.websearch.search_provider_placeholder')} placeholder={t('settings.websearch.search_provider_placeholder')}
options={providers options={providers.map((p) => ({
.toSorted((p1, p2) => p1.name.localeCompare(p2.name))
.map((p) => ({
value: p.id, value: p.id,
label: `${p.name} (${hasObjectKey(p, 'apiKey') ? 'ApiKey' : 'Free'})` label: `${p.name} (${hasObjectKey(p, 'apiKey') ? t('settings.websearch.apikey') : t('settings.websearch.free')})`
}))} }))}
/> />
</div> </div>
</SettingRow> </SettingRow>
</SettingGroup> </SettingGroup>
{!isLocalProvider && (
<SettingGroup theme={themeMode}> <SettingGroup theme={themeMode}>
{selectedProvider && <WebSearchProviderSetting provider={selectedProvider} />} {selectedProvider && <WebSearchProviderSetting provider={selectedProvider} />}
</SettingGroup> </SettingGroup>
)}
<BasicSettings /> <BasicSettings />
<BlacklistSettings /> <BlacklistSettings />
</SettingContainer> </SettingContainer>

View File

@ -83,7 +83,7 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
protected parseValidUrls(htmlContent: string): SearchItem[] { protected parseValidUrls(_htmlContent: string): SearchItem[] {
throw new Error('Not implemented') throw new Error('Not implemented')
} }

View File

@ -42,7 +42,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 94, version: 95,
blacklist: ['runtime', 'messages'], blacklist: ['runtime', 'messages'],
migrate migrate
}, },

View File

@ -14,6 +14,7 @@ import { RootState } from '.'
import { INITIAL_PROVIDERS, moveProvider } from './llm' import { INITIAL_PROVIDERS, moveProvider } from './llm'
import { mcpSlice } from './mcp' import { mcpSlice } from './mcp'
import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings' import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings'
import { defaultWebSearchProviders } from './websearch'
// remove logo base64 data to reduce the size of the state // remove logo base64 data to reduce the size of the state
function removeMiniAppIconsFromState(state: RootState) { function removeMiniAppIconsFromState(state: RootState) {
@ -52,6 +53,17 @@ function addProvider(state: RootState, id: string) {
} }
} }
function addWebSearchProvider(state: RootState, id: string) {
if (state.websearch && state.websearch.providers) {
if (!state.websearch.providers.find((p) => p.id === id)) {
const provider = defaultWebSearchProviders.find((p) => p.id === id)
if (provider) {
state.websearch.providers.push(provider)
}
}
}
}
const migrateConfig = { const migrateConfig = {
'2': (state: RootState) => { '2': (state: RootState) => {
try { try {
@ -985,21 +997,9 @@ const migrateConfig = {
}, },
'77': (state: RootState) => { '77': (state: RootState) => {
try { try {
addWebSearchProvider(state, 'searxng')
addWebSearchProvider(state, 'exa')
if (state.websearch) { if (state.websearch) {
if (!state.websearch.providers.find((p) => p.id === 'searxng')) {
state.websearch.providers.push(
{
id: 'searxng',
name: 'Searxng',
apiHost: ''
},
{
id: 'exa',
name: 'Exa',
apiKey: ''
}
)
}
state.websearch.providers.forEach((p) => { state.websearch.providers.forEach((p) => {
// @ts-ignore eslint-disable-next-line // @ts-ignore eslint-disable-next-line
delete p.enabled delete p.enabled
@ -1192,6 +1192,16 @@ const migrateConfig = {
} catch (error) { } catch (error) {
return state return state
} }
},
'95': (state: RootState) => {
try {
addWebSearchProvider(state, 'local-google')
addWebSearchProvider(state, 'local-bing')
addWebSearchProvider(state, 'local-baidu')
return state
} catch (error) {
return state
}
} }
} }

1478
yarn.lock

File diff suppressed because it is too large Load Diff