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:
parent
40182befe9
commit
062baad682
@ -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 { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
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 { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -13,15 +15,19 @@ import { SettingHelpLink, SettingHelpTextRow, SettingSubtitle, SettingTitle } fr
|
||||
interface Props {
|
||||
provider: WebSearchProvider
|
||||
}
|
||||
|
||||
const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const { provider, updateProvider } = useWebSearchProvider(_provider.id)
|
||||
const { t } = useTranslation()
|
||||
const [apiKey, setApiKey] = useState(provider.apiKey)
|
||||
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 apiKeyWebsite = webSearchProviderConfig?.websites?.apiKey
|
||||
const officialWebsite = webSearchProviderConfig?.websites?.official
|
||||
|
||||
const onUpdateApiKey = () => {
|
||||
if (apiKey !== 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(() => {
|
||||
setApiKey(provider.apiKey ?? '')
|
||||
setApiHost(provider.apiHost ?? '')
|
||||
@ -55,18 +101,27 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
</Flex>
|
||||
</SettingTitle>
|
||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||
{provider.apiKey !== undefined && (
|
||||
{hasObjectKey(provider, 'apiKey') && (
|
||||
<>
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Input.Password
|
||||
value={apiKey}
|
||||
placeholder={t('settings.provider.api_key')}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
onBlur={onUpdateApiKey}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
autoFocus={apiKey === ''}
|
||||
/>
|
||||
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Flex gap={8}>
|
||||
<Input.Password
|
||||
value={apiKey}
|
||||
placeholder={t('settings.provider.api_key')}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
onBlur={onUpdateApiKey}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
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 }}>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.websearch.get_api_key')}
|
||||
@ -74,9 +129,12 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
</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
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { CheckOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { WebSearchProvider } from '@renderer/types'
|
||||
import { Button, Select } from 'antd'
|
||||
import { Select } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -14,64 +12,19 @@ import WebSearchProviderSetting from './WebSearchProviderSetting'
|
||||
|
||||
const WebSearchSettings: FC = () => {
|
||||
const { providers } = useWebSearchProviders()
|
||||
const { provider: defaultProvider, setDefaultProvider, updateDefaultProvider } = useDefaultWebSearchProvider()
|
||||
|
||||
const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider()
|
||||
const { t } = useTranslation()
|
||||
const [selectedProvider, setSelectedProvider] = useState<WebSearchProvider | undefined>(defaultProvider)
|
||||
const { theme: themeMode } = useTheme()
|
||||
|
||||
const [apiChecking, setApiChecking] = useState(false)
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
|
||||
function updateSelectedWebSearchProvider(providerId: string) {
|
||||
const provider = providers.find((p) => p.id === providerId)
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
setApiValid(false)
|
||||
setSelectedProvider(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 (
|
||||
<SettingContainer theme={themeMode}>
|
||||
@ -88,16 +41,10 @@ const WebSearchSettings: FC = () => {
|
||||
placeholder={t('settings.websearch.search_provider_placeholder')}
|
||||
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>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={themeMode}>
|
||||
{selectedProvider && <WebSearchProviderSetting provider={selectedProvider} />}
|
||||
</SettingGroup>
|
||||
<BasicSettings />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import store from '@renderer/store'
|
||||
import { setDefaultProvider } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||
import { hasObjectKey } from '@renderer/utils'
|
||||
import WebSearchEngineProvider from '@renderer/webSearchProvider/WebSearchEngineProvider'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
@ -38,7 +39,20 @@ class WebSearchService {
|
||||
public isWebSearchEnabled(): boolean {
|
||||
const { defaultProvider, providers } = this.getWebSearchState()
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1214,16 +1214,22 @@ const migrateConfig = {
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
})
|
||||
const existWebsearchProvider = state.websearch.providers.find((p) => p.id === 'tavily')
|
||||
if (existWebsearchProvider && existWebsearchProvider.apiKey !== '') {
|
||||
existWebsearchProvider.enabled = true
|
||||
return state
|
||||
},
|
||||
'77': (state: RootState) => {
|
||||
if (state.websearch) {
|
||||
if (!state.websearch.providers.find((p) => p.id === 'searxng')) {
|
||||
state.websearch.providers.push({
|
||||
id: 'searxng',
|
||||
name: 'Searxng',
|
||||
apiHost: ''
|
||||
})
|
||||
}
|
||||
state.websearch.providers.forEach((p) => {
|
||||
// @ts-ignore eslint-disable-next-line
|
||||
delete p.enabled
|
||||
})
|
||||
}
|
||||
state.websearch.providers.push({
|
||||
id: 'searxng',
|
||||
name: 'Searxng',
|
||||
enabled: false,
|
||||
apiHost: ''
|
||||
})
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,6 @@ const websearchSlice = createSlice({
|
||||
updateWebSearchProviders: (state, action: PayloadAction<WebSearchProvider[]>) => {
|
||||
state.providers = action.payload
|
||||
},
|
||||
|
||||
updateWebSearchProvider: (state, action: PayloadAction<WebSearchProvider>) => {
|
||||
const index = state.providers.findIndex((provider) => provider.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
|
||||
@ -290,7 +290,6 @@ export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' |
|
||||
export type WebSearchProvider = {
|
||||
id: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
apiKey?: string
|
||||
apiHost?: string
|
||||
engines?: string[]
|
||||
|
||||
@ -474,4 +474,12 @@ export function getTitleFromString(str: string, length: number = 80) {
|
||||
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 }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user