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 { 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,18 +101,27 @@ 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>
|
||||||
<Input.Password
|
<Flex gap={8}>
|
||||||
value={apiKey}
|
<Input.Password
|
||||||
placeholder={t('settings.provider.api_key')}
|
value={apiKey}
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
placeholder={t('settings.provider.api_key')}
|
||||||
onBlur={onUpdateApiKey}
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
spellCheck={false}
|
onBlur={onUpdateApiKey}
|
||||||
type="password"
|
spellCheck={false}
|
||||||
autoFocus={apiKey === ''}
|
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 }}>
|
<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')}
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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({
|
||||||
|
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
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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[]
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user