feat: Refactor web search settings and remove enabled flag

- Remove `enabled` flag from WebSearchProvider type
- Add `hasObjectKey` utility function to check optional properties
- Update WebSearchService to check web search availability based on API key/host
- Modify WebSearchSettings and WebSearchProviderSetting components to support API key/host validation
- Add Searxng provider in migration script
- Simplify web search provider configuration and validation logic
This commit is contained in:
kangfenmao 2025-03-06 17:53:45 +08:00
parent 40182befe9
commit 062baad682
7 changed files with 115 additions and 84 deletions

View File

@ -1,8 +1,10 @@
import { ExportOutlined } from '@ant-design/icons' import { CheckOutlined, ExportOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders' import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
import WebSearchService from '@renderer/services/WebSearchService'
import { WebSearchProvider } from '@renderer/types' import { WebSearchProvider } from '@renderer/types'
import { Divider, Flex, Input } from 'antd' import { hasObjectKey } from '@renderer/utils'
import { Button, Divider, Flex, Input } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -13,15 +15,19 @@ import { SettingHelpLink, SettingHelpTextRow, SettingSubtitle, SettingTitle } fr
interface Props { interface Props {
provider: WebSearchProvider provider: WebSearchProvider
} }
const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => { const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
const { provider, updateProvider } = useWebSearchProvider(_provider.id) const { provider, updateProvider } = useWebSearchProvider(_provider.id)
const { t } = useTranslation() const { t } = useTranslation()
const [apiKey, setApiKey] = useState(provider.apiKey) const [apiKey, setApiKey] = useState(provider.apiKey)
const [apiHost, setApiHost] = useState(provider.apiHost) const [apiHost, setApiHost] = useState(provider.apiHost)
const [apiChecking, setApiChecking] = useState(false)
const [apiValid, setApiValid] = useState(false)
const webSearchProviderConfig = WEB_SEARCH_PROVIDER_CONFIG[provider.id] const webSearchProviderConfig = WEB_SEARCH_PROVIDER_CONFIG[provider.id]
const apiKeyWebsite = webSearchProviderConfig?.websites?.apiKey const apiKeyWebsite = webSearchProviderConfig?.websites?.apiKey
const officialWebsite = webSearchProviderConfig?.websites?.official const officialWebsite = webSearchProviderConfig?.websites?.official
const onUpdateApiKey = () => { const onUpdateApiKey = () => {
if (apiKey !== provider.apiKey) { if (apiKey !== provider.apiKey) {
updateProvider({ ...provider, apiKey }) updateProvider({ ...provider, apiKey })
@ -37,6 +43,46 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
} }
} }
async function checkSearch() {
if (!provider) {
window.message.error({
content: t('settings.websearch.no_provider_selected'),
duration: 3,
icon: <InfoCircleOutlined />,
key: 'no-provider-selected'
})
return
}
try {
setApiChecking(true)
const { valid, error } = await WebSearchService.checkSearch(provider)
setApiValid(valid)
if (!valid && error) {
const errorMessage = error.message ? ' ' + error.message : ''
window.message.error({
content: errorMessage,
duration: 4,
key: 'search-check-error'
})
}
updateProvider({ ...provider, enabled: true })
} catch (err) {
console.error('Check search error:', err)
setApiValid(false)
window.message.error({
content: t('settings.websearch.check_failed'),
duration: 3,
key: 'check-search-error'
})
} finally {
setApiChecking(false)
setTimeout(() => setApiValid(false), 2500)
}
}
useEffect(() => { useEffect(() => {
setApiKey(provider.apiKey ?? '') setApiKey(provider.apiKey ?? '')
setApiHost(provider.apiHost ?? '') setApiHost(provider.apiHost ?? '')
@ -55,9 +101,10 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
</Flex> </Flex>
</SettingTitle> </SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} /> <Divider style={{ width: '100%', margin: '10px 0' }} />
{provider.apiKey !== undefined && ( {hasObjectKey(provider, 'apiKey') && (
<> <>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle> <SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Flex gap={8}>
<Input.Password <Input.Password
value={apiKey} value={apiKey}
placeholder={t('settings.provider.api_key')} placeholder={t('settings.provider.api_key')}
@ -67,6 +114,14 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
type="password" type="password"
autoFocus={apiKey === ''} autoFocus={apiKey === ''}
/> />
<Button
ghost={apiValid}
type={apiValid ? 'primary' : 'default'}
onClick={checkSearch}
disabled={apiChecking}>
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.websearch.check')}
</Button>
</Flex>
<SettingHelpTextRow style={{ justifyContent: 'space-between', marginTop: 5 }}> <SettingHelpTextRow style={{ justifyContent: 'space-between', marginTop: 5 }}>
<SettingHelpLink target="_blank" href={apiKeyWebsite}> <SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.websearch.get_api_key')} {t('settings.websearch.get_api_key')}
@ -74,9 +129,12 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
</SettingHelpTextRow> </SettingHelpTextRow>
</> </>
)} )}
{provider.apiHost !== undefined && (
{hasObjectKey(provider, 'apiHost') && (
<> <>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_host')}</SettingSubtitle> <SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
{t('settings.provider.api_host')}
</SettingSubtitle>
<Input <Input
value={apiHost} value={apiHost}
placeholder={t('settings.provider.api_host')} placeholder={t('settings.provider.api_host')}

View File

@ -1,9 +1,7 @@
import { CheckOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
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 WebSearchService from '@renderer/services/WebSearchService'
import { WebSearchProvider } from '@renderer/types' import { WebSearchProvider } from '@renderer/types'
import { Button, Select } from 'antd' import { Select } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -14,64 +12,19 @@ import WebSearchProviderSetting from './WebSearchProviderSetting'
const WebSearchSettings: FC = () => { const WebSearchSettings: FC = () => {
const { providers } = useWebSearchProviders() const { providers } = useWebSearchProviders()
const { provider: defaultProvider, setDefaultProvider, updateDefaultProvider } = 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()
const [apiChecking, setApiChecking] = useState(false)
const [apiValid, setApiValid] = useState(false)
function updateSelectedWebSearchProvider(providerId: string) { function updateSelectedWebSearchProvider(providerId: string) {
const provider = providers.find((p) => p.id === providerId) const provider = providers.find((p) => p.id === providerId)
if (!provider) { if (!provider) {
return return
} }
setApiValid(false)
setSelectedProvider(provider) setSelectedProvider(provider)
setDefaultProvider(provider) setDefaultProvider(provider)
} }
async function checkSearch() {
// 检查是否选择了提供商
if (!selectedProvider || !selectedProvider.id) {
window.message.error({
content: t('settings.websearch.no_provider_selected'),
duration: 3,
icon: <InfoCircleOutlined />,
key: 'no-provider-selected'
})
return
}
try {
setApiChecking(true)
const { valid, error } = await WebSearchService.checkSearch(selectedProvider)
setApiValid(valid)
// 如果验证失败且有错误信息,显示错误
if (!valid && error) {
const errorMessage = error.message ? ' ' + error.message : ''
window.message.error({
content: errorMessage,
duration: 4,
key: 'search-check-error'
})
}
updateDefaultProvider({ ...selectedProvider, enabled: true })
} catch (err) {
console.error('Check search error:', err)
setApiValid(false)
window.message.error({
content: t('settings.websearch.check_failed'),
duration: 3,
key: 'check-search-error'
})
} finally {
setApiChecking(false)
}
}
return ( return (
<SettingContainer theme={themeMode}> <SettingContainer theme={themeMode}>
@ -88,16 +41,10 @@ const WebSearchSettings: FC = () => {
placeholder={t('settings.websearch.search_provider_placeholder')} placeholder={t('settings.websearch.search_provider_placeholder')}
options={providers.map((p) => ({ value: p.id, label: p.name }))} options={providers.map((p) => ({ value: p.id, label: p.name }))}
/> />
<Button
ghost={apiValid}
type={apiValid ? 'primary' : 'default'}
onClick={async () => await checkSearch()}
disabled={apiChecking}>
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.websearch.check')}
</Button>
</div> </div>
</SettingRow> </SettingRow>
<SettingDivider /> </SettingGroup>
<SettingGroup theme={themeMode}>
{selectedProvider && <WebSearchProviderSetting provider={selectedProvider} />} {selectedProvider && <WebSearchProviderSetting provider={selectedProvider} />}
</SettingGroup> </SettingGroup>
<BasicSettings /> <BasicSettings />

View File

@ -1,6 +1,7 @@
import store from '@renderer/store' import store from '@renderer/store'
import { setDefaultProvider } from '@renderer/store/websearch' import { setDefaultProvider } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types' import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import WebSearchEngineProvider from '@renderer/webSearchProvider/WebSearchEngineProvider' import WebSearchEngineProvider from '@renderer/webSearchProvider/WebSearchEngineProvider'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -38,7 +39,20 @@ class WebSearchService {
public isWebSearchEnabled(): boolean { public isWebSearchEnabled(): boolean {
const { defaultProvider, providers } = this.getWebSearchState() const { defaultProvider, providers } = this.getWebSearchState()
const provider = providers.find((provider) => provider.id === defaultProvider) const provider = providers.find((provider) => provider.id === defaultProvider)
return provider?.enabled ?? false
if (!provider) {
return false
}
if (hasObjectKey(provider, 'apiKey')) {
return provider.apiKey !== ''
}
if (hasObjectKey(provider, 'apiHost')) {
return provider.apiHost !== ''
}
return false
} }
/** /**

View File

@ -1214,16 +1214,22 @@ const migrateConfig = {
isSystem: true, isSystem: true,
enabled: false enabled: false
}) })
const existWebsearchProvider = state.websearch.providers.find((p) => p.id === 'tavily') return state
if (existWebsearchProvider && existWebsearchProvider.apiKey !== '') { },
existWebsearchProvider.enabled = true '77': (state: RootState) => {
} if (state.websearch) {
if (!state.websearch.providers.find((p) => p.id === 'searxng')) {
state.websearch.providers.push({ state.websearch.providers.push({
id: 'searxng', id: 'searxng',
name: 'Searxng', name: 'Searxng',
enabled: false,
apiHost: '' apiHost: ''
}) })
}
state.websearch.providers.forEach((p) => {
// @ts-ignore eslint-disable-next-line
delete p.enabled
})
}
return state return state
} }
} }

View File

@ -42,7 +42,6 @@ const websearchSlice = createSlice({
updateWebSearchProviders: (state, action: PayloadAction<WebSearchProvider[]>) => { updateWebSearchProviders: (state, action: PayloadAction<WebSearchProvider[]>) => {
state.providers = action.payload state.providers = action.payload
}, },
updateWebSearchProvider: (state, action: PayloadAction<WebSearchProvider>) => { updateWebSearchProvider: (state, action: PayloadAction<WebSearchProvider>) => {
const index = state.providers.findIndex((provider) => provider.id === action.payload.id) const index = state.providers.findIndex((provider) => provider.id === action.payload.id)
if (index !== -1) { if (index !== -1) {

View File

@ -290,7 +290,6 @@ export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' |
export type WebSearchProvider = { export type WebSearchProvider = {
id: string id: string
name: string name: string
enabled: boolean
apiKey?: string apiKey?: string
apiHost?: string apiHost?: string
engines?: string[] engines?: string[]

View File

@ -474,4 +474,12 @@ export function getTitleFromString(str: string, length: number = 80) {
return title return title
} }
export function hasObjectKey(obj: any, key: string) {
if (typeof obj !== 'object' || obj === null) {
return false
}
return Object.keys(obj).includes(key)
}
export { classNames } export { classNames }