mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-27 22:22:00 +08:00
* feat(类型): 添加WebSearchProviderIds常量并更新WebSearchProvider类型 * refactor(web-search): 重构网络搜索提供商配置和logo获取逻辑 将webSearchProviders.ts中的提供商logo获取函数移动到使用组件中 并优化提供商配置的类型定义 * feat(WebSearchButton): 添加不同搜索引擎的图标支持 为WebSearchButton组件添加多个搜索引擎的图标支持,包括Baidu、Google、Bing等 * feat(types): 添加预处理和网页搜索提供者的类型校验函数 添加 PreprocessProviderId 和 WebSearchProviderId 的类型校验函数 isPreprocessProviderId 和 isWebSearchProviderId,用于验证字符串是否为有效的提供者 ID * refactor(types): 重命名ApiProviderUnion并添加更新函数类型 添加用于更新不同类型API提供者的函数类型,提高类型安全性 * refactor(websearch): 将搜索提供商配置提取到单独文件 将websearch store中的搜索提供商配置提取到单独的配置文件,提高代码可维护性 * refactor(PreprocessSettings): 移除未使用的 system 选项禁用逻辑 由于 system 字段实际未使用,移除相关代码以简化逻辑 * refactor(api-key-popup): 移除providerKind参数,改用providerId判断类型 * refactor(preprocessProviders): 使用类型定义优化预处理提供者配置 将 providerId 参数类型从 string 改为 PreprocessProviderId 为 PREPROCESS_PROVIDER_CONFIG 添加类型定义 * refactor(hooks): 使用PreprocessProviderId类型替换字符串类型参数 * refactor(hooks): 使用 WebSearchProviderId 类型替换字符串类型参数 将 useWebSearchProvider 钩子的 id 参数类型从 string 改为 WebSearchProviderId,提高类型安全性 * refactor(knowledge): 将providerId类型改为PreprocessProviderId * refactor(PreprocessSettings): 移除未使用的options相关代码 清理PreprocessSettings组件中已被注释掉的options状态和相关逻辑,简化代码结构 * refactor(WebSearchProviderSetting): 将providerId类型从string改为WebSearchProviderId * refactor(websearch): 移除WebSearchProvider类型中不必要的id字段约束 * style(WebSearchButton): 调整图标大小和样式以保持视觉一致性 * fix(ApiKeyListPopup): 修正LLM提供者判断逻辑 使用'models'属性检查替代原有逻辑,更准确地判断是否为LLM provider * fix(ApiKeyListPopup): 修复预处理provider判断逻辑 处理mistral同时提供预处理和llm服务的情况,避免误判
294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
import { CheckOutlined, ExportOutlined, LoadingOutlined } from '@ant-design/icons'
|
|
import { loggerService } from '@logger'
|
|
import BochaLogo from '@renderer/assets/images/search/bocha.webp'
|
|
import ExaLogo from '@renderer/assets/images/search/exa.png'
|
|
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
|
|
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
|
|
import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup'
|
|
import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
|
|
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
|
|
import WebSearchService from '@renderer/services/WebSearchService'
|
|
import { WebSearchProviderId } from '@renderer/types'
|
|
import { formatApiKeys, hasObjectKey } from '@renderer/utils'
|
|
import { Button, Divider, Flex, Form, Input, Space, Tooltip } from 'antd'
|
|
import Link from 'antd/es/typography/Link'
|
|
import { Info, List } from 'lucide-react'
|
|
import { FC, useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import styled from 'styled-components'
|
|
|
|
import { SettingDivider, SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
|
|
|
|
const logger = loggerService.withContext('WebSearchProviderSetting')
|
|
interface Props {
|
|
providerId: WebSearchProviderId
|
|
}
|
|
|
|
const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
|
|
const { provider, updateProvider } = useWebSearchProvider(providerId)
|
|
const { t } = useTranslation()
|
|
const [apiKey, setApiKey] = useState(provider.apiKey || '')
|
|
const [apiHost, setApiHost] = useState(provider.apiHost || '')
|
|
const [apiChecking, setApiChecking] = useState(false)
|
|
const [basicAuthUsername, setBasicAuthUsername] = useState(provider.basicAuthUsername || '')
|
|
const [basicAuthPassword, setBasicAuthPassword] = useState(provider.basicAuthPassword || '')
|
|
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({ apiKey })
|
|
}
|
|
}
|
|
|
|
const onUpdateApiHost = () => {
|
|
let trimmedHost = apiHost?.trim() || ''
|
|
if (trimmedHost.endsWith('/')) {
|
|
trimmedHost = trimmedHost.slice(0, -1)
|
|
}
|
|
if (trimmedHost !== provider.apiHost) {
|
|
updateProvider({ apiHost: trimmedHost })
|
|
} else {
|
|
setApiHost(provider.apiHost || '')
|
|
}
|
|
}
|
|
|
|
const onUpdateBasicAuthUsername = () => {
|
|
const currentValue = basicAuthUsername || ''
|
|
const savedValue = provider.basicAuthUsername || ''
|
|
if (currentValue !== savedValue) {
|
|
updateProvider({ basicAuthUsername })
|
|
} else {
|
|
setBasicAuthUsername(provider.basicAuthUsername || '')
|
|
}
|
|
}
|
|
|
|
const onUpdateBasicAuthPassword = () => {
|
|
const currentValue = basicAuthPassword || ''
|
|
const savedValue = provider.basicAuthPassword || ''
|
|
if (currentValue !== savedValue) {
|
|
updateProvider({ basicAuthPassword })
|
|
} else {
|
|
setBasicAuthPassword(provider.basicAuthPassword || '')
|
|
}
|
|
}
|
|
|
|
const openApiKeyList = async () => {
|
|
await ApiKeyListPopup.show({
|
|
providerId: provider.id,
|
|
title: `${provider.name} ${t('settings.provider.api.key.list.title')}`
|
|
})
|
|
}
|
|
|
|
async function checkSearch() {
|
|
if (!provider) {
|
|
window.message.error({
|
|
content: t('settings.no_provider_selected'),
|
|
duration: 3,
|
|
icon: <Info size={18} />,
|
|
key: 'no-provider-selected'
|
|
})
|
|
return
|
|
}
|
|
|
|
if (apiKey.includes(',')) {
|
|
await openApiKeyList()
|
|
return
|
|
}
|
|
|
|
try {
|
|
setApiChecking(true)
|
|
const { valid, error } = await WebSearchService.checkSearch(provider)
|
|
|
|
const errorMessage = error && error?.message ? ' ' + error?.message : ''
|
|
window.message[valid ? 'success' : 'error']({
|
|
key: 'api-check',
|
|
style: { marginTop: '3vh' },
|
|
duration: valid ? 2 : 8,
|
|
content: valid
|
|
? t('settings.tool.websearch.check_success')
|
|
: t('settings.tool.websearch.check_failed') + errorMessage
|
|
})
|
|
|
|
setApiValid(valid)
|
|
} catch (err) {
|
|
logger.error('Check search error:', err as Error)
|
|
setApiValid(false)
|
|
window.message.error({
|
|
key: 'check-search-error',
|
|
style: { marginTop: '3vh' },
|
|
duration: 8,
|
|
content: t('settings.tool.websearch.check_failed')
|
|
})
|
|
} finally {
|
|
setApiChecking(false)
|
|
setTimeout(() => setApiValid(false), 2500)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
setApiKey(provider.apiKey ?? '')
|
|
setApiHost(provider.apiHost ?? '')
|
|
setBasicAuthUsername(provider.basicAuthUsername ?? '')
|
|
setBasicAuthPassword(provider.basicAuthPassword ?? '')
|
|
}, [provider.apiKey, provider.apiHost, provider.basicAuthUsername, provider.basicAuthPassword])
|
|
|
|
const getWebSearchProviderLogo = (providerId: WebSearchProviderId) => {
|
|
switch (providerId) {
|
|
case 'tavily':
|
|
return TavilyLogo
|
|
case 'searxng':
|
|
return SearxngLogo
|
|
case 'exa':
|
|
return ExaLogo
|
|
case 'bocha':
|
|
return BochaLogo
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<SettingTitle>
|
|
<Flex align="center" gap={8}>
|
|
<ProviderLogo src={getWebSearchProviderLogo(provider.id)} />
|
|
<ProviderName> {provider.name}</ProviderName>
|
|
{officialWebsite && webSearchProviderConfig?.websites && (
|
|
<Link target="_blank" href={webSearchProviderConfig.websites.official}>
|
|
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
|
|
</Link>
|
|
)}
|
|
</Flex>
|
|
</SettingTitle>
|
|
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
|
{hasObjectKey(provider, 'apiKey') && (
|
|
<>
|
|
<SettingSubtitle
|
|
style={{
|
|
marginTop: 5,
|
|
marginBottom: 10,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between'
|
|
}}>
|
|
{t('settings.provider.api_key.label')}
|
|
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
|
|
<Button type="text" size="small" onClick={openApiKeyList} icon={<List size={14} />} />
|
|
</Tooltip>
|
|
</SettingSubtitle>
|
|
<Space.Compact style={{ width: '100%' }}>
|
|
<Input.Password
|
|
value={apiKey}
|
|
placeholder={t('settings.provider.api_key.label')}
|
|
onChange={(e) => setApiKey(formatApiKeys(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.tool.websearch.check')
|
|
)}
|
|
</Button>
|
|
</Space.Compact>
|
|
<SettingHelpTextRow style={{ justifyContent: 'space-between', marginTop: 5 }}>
|
|
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
|
{t('settings.provider.api_key.tip')}
|
|
</SettingHelpLink>
|
|
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
|
</SettingHelpTextRow>
|
|
</>
|
|
)}
|
|
{hasObjectKey(provider, 'apiHost') && (
|
|
<>
|
|
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
|
|
{t('settings.provider.api_host')}
|
|
</SettingSubtitle>
|
|
<Flex gap={8}>
|
|
<Input
|
|
value={apiHost}
|
|
placeholder={t('settings.provider.api_host')}
|
|
onChange={(e) => setApiHost(e.target.value)}
|
|
onBlur={onUpdateApiHost}
|
|
/>
|
|
</Flex>
|
|
</>
|
|
)}
|
|
{hasObjectKey(provider, 'basicAuthUsername') && (
|
|
<>
|
|
<SettingDivider style={{ marginTop: 12, marginBottom: 12 }} />
|
|
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
|
|
{t('settings.provider.basic_auth.label')}
|
|
<Tooltip title={t('settings.provider.basic_auth.tip')} placement="right">
|
|
<Info size={16} color="var(--color-icon)" style={{ marginLeft: 5, cursor: 'pointer' }} />
|
|
</Tooltip>
|
|
</SettingSubtitle>
|
|
<Flex>
|
|
<Form
|
|
layout="vertical"
|
|
style={{ width: '100%' }}
|
|
initialValues={{
|
|
username: basicAuthUsername,
|
|
password: basicAuthPassword
|
|
}}
|
|
onValuesChange={(changedValues) => {
|
|
// Update local state when form values change
|
|
if ('username' in changedValues) {
|
|
setBasicAuthUsername(changedValues.username || '')
|
|
}
|
|
if ('password' in changedValues) {
|
|
setBasicAuthPassword(changedValues.password || '')
|
|
}
|
|
}}>
|
|
<Form.Item label={t('settings.provider.basic_auth.user_name.label')} name="username">
|
|
<Input
|
|
placeholder={t('settings.provider.basic_auth.user_name.tip')}
|
|
onBlur={onUpdateBasicAuthUsername}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('settings.provider.basic_auth.password.label')}
|
|
name="password"
|
|
rules={[{ required: !!basicAuthUsername, validateTrigger: ['onBlur', 'onChange'] }]}
|
|
help=""
|
|
hidden={!basicAuthUsername}>
|
|
<Input.Password
|
|
placeholder={t('settings.provider.basic_auth.password.tip')}
|
|
onBlur={onUpdateBasicAuthPassword}
|
|
disabled={!basicAuthUsername}
|
|
visibilityToggle={true}
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Flex>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const ProviderName = styled.span`
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
`
|
|
const ProviderLogo = styled.img`
|
|
width: 20px;
|
|
height: 20px;
|
|
object-fit: contain;
|
|
`
|
|
|
|
export default WebSearchProviderSetting
|