feat: web search icons (#9147)

* 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服务的情况,避免误判
This commit is contained in:
Phantom 2025-08-14 23:19:17 +08:00 committed by GitHub
parent 31e59ab395
commit bef0180e4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 359 additions and 187 deletions

View File

@ -112,3 +112,129 @@ export function MdiLightbulbOn(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function BingLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill="currentColor"
fill-rule="evenodd"
width="1em"
height="1em"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M4.842.005a.966.966 0 01.604.142l2.62 1.813c.369.256.492.352.637.496.471.47.752 1.09.797 1.765l.008.847.003 1.441.004 13.002.144-.094 7.015-4.353.015.003.029.01c-.398-.17-.893-.339-1.655-.566l-.484-.146c-.584-.18-.71-.238-.921-.38a2.009 2.009 0 01-.37-.312 2.172 2.172 0 01-.41-.592L11.32 9.063c-.166-.444-.166-.49-.156-.63a.92.92 0 01.806-.864l.094-.01c.044-.005.22.023.29.044l.052.021c.06.026.16.075.313.154l3.63 1.908a6.626 6.626 0 013.292 4.531c.194.99.159 2.037-.102 3.012-.216.805-.639 1.694-1.054 2.213l-.08.099-.047.05c-.01.01-.013.01-.01.002l.043-.074-.072.114c-.011.031-.233.28-.38.425l-.17.161c-.22.202-.431.36-.832.62L13.544 23c-.941.6-1.86.912-2.913.992-.23.018-.854.008-1.074-.017a6.31 6.31 0 01-1.658-.412c-1.854-.738-3.223-2.288-3.705-4.195a8.077 8.077 0 01-.121-.57l-.046-.325a1.123 1.123 0 01-.014-.168l-.006-.029L4 11.617 4.01.866a.981.981 0 01.007-.111.943.943 0 01.825-.75z"></path>
</svg>
)
}
export function SearXNGLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 265 265" style={{ display: 'block' }} {...props}>
<g transform="translate(-40.921 -17.417)">
<circle
cx="142.2"
cy="122.9"
r="85"
fill="none"
stroke="currentColor"
strokeWidth="28.3465"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="11.3386"
/>
<path
d="M118.4 77.6c19.8-10.2 44-6.4 59.7 9.4s19.3 40 8.9 59.7"
fill="none"
stroke="currentColor"
strokeWidth="14.1732"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="11.3386"
/>
<path d="m184.2 202 37-38.6 81.8 78.3-37 38.6z" fill="currentColor" />
</g>
</svg>
)
}
export function TavilyLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="m16.44.964 4.921 7.79c.79 1.252-.108 2.883-1.588 2.883H17.76V23.3h-2.91V.088c.61 0 1.22.292 1.59.876z"
fill="currentColor"
/>
<path
d="M8.342 8.755 13.263.964a1.864 1.864 0 0 1 1.59-.876V23.3a4.87 4.87 0 0 0-.252-.006c-.99 0-1.907.311-2.658.842V11.637H9.93c-1.48 0-2.38-1.631-1.589-2.882z"
fill="currentColor"
/>
<path
d="M30.278 31H18.031a4.596 4.596 0 0 0 1.219-2.91h22.577c0 .61-.292 1.22-.875 1.59L33.16 34.6c-1.251.791-2.883-.108-2.883-1.588V31z"
fill="currentColor"
/>
<path
d="m33.16 21.581 7.79 4.921c.585.369.876.979.876 1.589H19.25a4.619 4.619 0 0 0-.858-2.91h11.887V23.17c0-1.48 1.631-2.38 2.882-1.589z"
fill="currentColor"
/>
<path
d="m8.24 34.25-7.107 7.108a1.864 1.864 0 0 0 1.742.504l8.989-2.03c1.443-.325 1.961-2.114.915-3.16l-1.423-1.423 5.356-5.356a2.805 2.805 0 0 0 0-3.966l-.074-.075L8.24 34.25z"
fill="currentColor"
/>
<path
d="m7.243 31.135 5.355-5.356a2.805 2.805 0 0 1 3.967 0l.074.074-8.397 8.397-7.108 7.108a1.864 1.864 0 0 1-.504-1.742l2.029-8.989c.325-1.444 2.115-1.961 3.161-.915l1.423 1.423z"
fill="currentColor"
/>
</svg>
)
}
export function ExaLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill="currentColor"
fill-rule="evenodd"
width="1em"
height="1em"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<title>Exa</title>
<path
clip-rule="evenodd"
d="M3 0h19v1.791L13.892 12 22 22.209V24H3V0zm9.62 10.348l6.589-8.557H6.03l6.59 8.557zM5.138 3.935v7.17h5.52l-5.52-7.17zm5.52 8.96h-5.52v7.17l5.52-7.17zM6.03 22.21l6.59-8.557 6.589 8.557H6.03z"></path>
</svg>
)
}
export function BochaLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg width="1em" height="1em" viewBox="0 0 135 116" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.5754 13.8123C24.6109 7.94459 39.1223 12.9435 44.9955 24.9805L57.5355 50.6805C60.4695 56.6936 57.9756 63.9478 51.9652 66.8832C51.9627 66.8844 51.9602 66.8856 51.9577 66.8868C45.94 69.8206 38.6843 67.3212 35.7477 61.3027L12.5754 13.8123Z"
fill="currentColor"
/>
<path
opacity="0.64774"
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 38.3013C9.46916 28.836 24.813 28.836 34.2822 38.3013L55.2526 59.2631C59.9819 63.9904 59.9852 71.6582 55.2601 76.3896C55.2576 76.3921 55.2551 76.3946 55.2526 76.397C50.5181 81.1297 42.8461 81.1297 38.1116 76.397L0 38.3013Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M86.8777 18.0444C113.939 18.0444 135.876 39.9725 135.876 67.0222C135.876 80.2286 129.086 93.6477 120.585 102.457L117.065 98.2367C111.026 90.9998 108.882 81.2777 111.314 72.1702C111.755 70.5198 111.976 69.0033 111.976 67.6209C111.976 53.6689 100.661 42.3586 86.7029 42.3586C72.7452 42.3586 61.4303 53.6689 61.4303 67.6209C61.4303 81.5728 72.7452 92.8831 86.7029 92.8831C89.3159 92.8831 91.8363 92.4867 94.2071 91.7508C101.312 89.5455 109.054 91.3768 114.419 96.5322L120.585 102.457C111.83 110.626 99.7992 116 86.8777 116C59.8168 116 37.8796 94.0719 37.8796 67.0222C37.8796 39.9725 59.8168 18.0444 86.8777 18.0444Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M37.8796 0C51.2677 0 62.1208 10.8581 62.1208 24.2522V41.7389C62.1208 55.133 51.2677 65.9911 37.8796 65.9911V0Z"
fill="currentColor"
/>
</svg>
)
}

View File

@ -3,7 +3,14 @@ import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import SelectProviderModelPopup from '@renderer/pages/settings/ProviderSettings/SelectProviderModelPopup'
import { checkApi } from '@renderer/services/ApiService'
import WebSearchService from '@renderer/services/WebSearchService'
import { Model, PreprocessProvider, Provider, WebSearchProvider } from '@renderer/types'
import {
isPreprocessProviderId,
isWebSearchProviderId,
Model,
PreprocessProvider,
Provider,
WebSearchProvider
} from '@renderer/types'
import { ApiKeyConnectivity, ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck'
import { formatApiKeys, splitApiKeyString } from '@renderer/utils/api'
import { formatErrorMessage } from '@renderer/utils/error'
@ -12,12 +19,11 @@ import { isEmpty } from 'lodash'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ApiKeyValidity, ApiProviderKind, ApiProviderUnion } from './types'
import { ApiKeyValidity, ApiProvider, UpdateApiProviderFunc } from './types'
interface UseApiKeysProps {
provider: ApiProviderUnion
updateProvider: (provider: Partial<ApiProviderUnion>) => void
providerKind: ApiProviderKind
provider: ApiProvider
updateProvider: UpdateApiProviderFunc
}
const logger = loggerService.withContext('ApiKeyListPopup')
@ -25,7 +31,7 @@ const logger = loggerService.withContext('ApiKeyListPopup')
/**
* API Keys hook
*/
export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKeysProps) {
export function useApiKeys({ provider, updateProvider }: UseApiKeysProps) {
const { t } = useTranslation()
// 连通性检查的 UI 状态管理
@ -199,11 +205,13 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
try {
const startTime = Date.now()
if (isLlmProvider(provider, providerKind) && model) {
if (isLlmProvider(provider) && model) {
await checkApi({ ...provider, apiKey: keyToCheck }, model)
} else {
} else if (isWebSearchProvider(provider)) {
const result = await WebSearchService.checkSearch({ ...provider, apiKey: keyToCheck })
if (!result.valid) throw new Error(result.error)
} else {
// 不处理预处理供应商
}
const latency = Date.now() - startTime
@ -228,7 +236,7 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
logger.error('failed to validate the connectivity of the api key', error)
}
},
[keys, connectivityStates, updateConnectivityState, provider, providerKind]
[keys, connectivityStates, updateConnectivityState, provider]
)
// 检查单个 key 的连通性
@ -240,23 +248,23 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
const currentState = connectivityStates.get(keyToCheck)
if (currentState?.checking) return
const model = isLlmProvider(provider, providerKind) ? await getModelForCheck(provider, t) : undefined
const model = isLlmProvider(provider) ? await getModelForCheck(provider, t) : undefined
if (model === null) return
await runConnectivityCheck(index, model)
},
[provider, keys, connectivityStates, providerKind, t, runConnectivityCheck]
[provider, keys, connectivityStates, t, runConnectivityCheck]
)
// 检查所有 keys 的连通性
const checkAllKeysConnectivity = useCallback(async () => {
if (!provider || keys.length === 0) return
const model = isLlmProvider(provider, providerKind) ? await getModelForCheck(provider, t) : undefined
const model = isLlmProvider(provider) ? await getModelForCheck(provider, t) : undefined
if (model === null) return
await Promise.allSettled(keys.map((_, index) => runConnectivityCheck(index, model)))
}, [provider, keys, providerKind, t, runConnectivityCheck])
}, [provider, keys, t, runConnectivityCheck])
// 计算是否有 key 正在检查
const isChecking = useMemo(() => {
@ -275,16 +283,18 @@ export function useApiKeys({ provider, updateProvider, providerKind }: UseApiKey
}
}
export function isLlmProvider(obj: any, kind: ApiProviderKind): obj is Provider {
return kind === 'llm' && 'type' in obj && 'models' in obj
export function isLlmProvider(provider: ApiProvider): provider is Provider {
return 'models' in provider
}
export function isWebSearchProvider(obj: any, kind: ApiProviderKind): obj is WebSearchProvider {
return kind === 'websearch' && ('url' in obj || 'engines' in obj)
export function isWebSearchProvider(provider: ApiProvider): provider is WebSearchProvider {
return isWebSearchProviderId(provider.id)
}
export function isPreprocessProvider(obj: any, kind: ApiProviderKind): obj is PreprocessProvider {
return kind === 'doc-preprocess' && ('quota' in obj || 'options' in obj)
export function isPreprocessProvider(provider: ApiProvider): provider is PreprocessProvider {
// NOTE: mistral 同时提供预处理和llm服务所以其llm provier可能被误判为预处理provider
// 后面需要使用更严格的判断方式
return isPreprocessProviderId(provider.id) && !isLlmProvider(provider)
}
// 获取模型用于检查

View File

@ -6,6 +6,7 @@ import { useProvider } from '@renderer/hooks/useProvider'
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
import { SettingHelpText } from '@renderer/pages/settings'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { PreprocessProviderId, WebSearchProviderId } from '@renderer/types'
import { ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck'
import { Button, Card, Flex, List, Popconfirm, Space, Tooltip, Typography } from 'antd'
import { Plus } from 'lucide-react'
@ -15,19 +16,18 @@ import styled from 'styled-components'
import { isLlmProvider, useApiKeys } from './hook'
import ApiKeyItem from './item'
import { ApiProviderKind, ApiProviderUnion } from './types'
import { ApiProvider, UpdateApiProviderFunc } from './types'
interface ApiKeyListProps {
provider: ApiProviderUnion
updateProvider: (provider: Partial<ApiProviderUnion>) => void
providerKind: ApiProviderKind
provider: ApiProvider
updateProvider: UpdateApiProviderFunc
showHealthCheck?: boolean
}
/**
* Api key CRUD
*/
export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, providerKind, showHealthCheck = true }) => {
export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, showHealthCheck = true }) => {
const { t } = useTranslation()
// 临时新项状态
@ -42,7 +42,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
checkKeyConnectivity,
checkAllKeysConnectivity,
isChecking
} = useApiKeys({ provider, updateProvider, providerKind: providerKind })
} = useApiKeys({ provider, updateProvider })
// 创建一个临时新项
const handleAddNew = () => {
@ -73,7 +73,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
const shouldAutoFocus = () => {
if (provider.apiKey) return false
return isLlmProvider(provider, providerKind) && provider.enabled && !isProviderSupportAuth(provider)
return isLlmProvider(provider) && provider.enabled && !isProviderSupportAuth(provider)
}
// 合并真实 keys 和临时新项
@ -179,55 +179,33 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
interface SpecificApiKeyListProps {
providerId: string
providerKind: ApiProviderKind
showHealthCheck?: boolean
}
export const LlmApiKeyList: FC<SpecificApiKeyListProps> = ({ providerId, providerKind, showHealthCheck = true }) => {
type WebSearchApiKeyList = SpecificApiKeyListProps & {
providerId: WebSearchProviderId
}
type DocPreprocessApiKeyListProps = SpecificApiKeyListProps & {
providerId: PreprocessProviderId
}
export const LlmApiKeyList: FC<SpecificApiKeyListProps> = ({ providerId, showHealthCheck = true }) => {
const { provider, updateProvider } = useProvider(providerId)
return (
<ApiKeyList
provider={provider}
updateProvider={updateProvider}
providerKind={providerKind}
showHealthCheck={showHealthCheck}
/>
)
return <ApiKeyList provider={provider} updateProvider={updateProvider} showHealthCheck={showHealthCheck} />
}
export const WebSearchApiKeyList: FC<SpecificApiKeyListProps> = ({
providerId,
providerKind,
showHealthCheck = true
}) => {
export const WebSearchApiKeyList: FC<WebSearchApiKeyList> = ({ providerId, showHealthCheck = true }) => {
const { provider, updateProvider } = useWebSearchProvider(providerId)
return (
<ApiKeyList
provider={provider}
updateProvider={updateProvider}
providerKind={providerKind}
showHealthCheck={showHealthCheck}
/>
)
return <ApiKeyList provider={provider} updateProvider={updateProvider} showHealthCheck={showHealthCheck} />
}
export const DocPreprocessApiKeyList: FC<SpecificApiKeyListProps> = ({
providerId,
providerKind,
showHealthCheck = true
}) => {
export const DocPreprocessApiKeyList: FC<DocPreprocessApiKeyListProps> = ({ providerId, showHealthCheck = true }) => {
const { provider, updateProvider } = usePreprocessProvider(providerId)
return (
<ApiKeyList
provider={provider}
updateProvider={updateProvider}
providerKind={providerKind}
showHealthCheck={showHealthCheck}
/>
)
return <ApiKeyList provider={provider} updateProvider={updateProvider} showHealthCheck={showHealthCheck} />
}
const ListContainer = styled.div`

View File

@ -1,14 +1,13 @@
import { TopView } from '@renderer/components/TopView'
import { isPreprocessProviderId, isWebSearchProviderId } from '@renderer/types'
import { Modal } from 'antd'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DocPreprocessApiKeyList, LlmApiKeyList, WebSearchApiKeyList } from './list'
import { ApiProviderKind } from './types'
interface ShowParams {
providerId: string
providerKind: ApiProviderKind
title?: string
showHealthCheck?: boolean
}
@ -20,7 +19,7 @@ interface Props extends ShowParams {
/**
* API Key
*/
const PopupContainer: React.FC<Props> = ({ providerId, providerKind, title, resolve, showHealthCheck = true }) => {
const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealthCheck = true }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
@ -33,17 +32,14 @@ const PopupContainer: React.FC<Props> = ({ providerId, providerKind, title, reso
}
const ListComponent = useMemo(() => {
switch (providerKind) {
case 'llm':
return LlmApiKeyList
case 'websearch':
return WebSearchApiKeyList
case 'doc-preprocess':
return DocPreprocessApiKeyList
default:
return null
if (isWebSearchProviderId(providerId)) {
return <WebSearchApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}
}, [providerKind])
if (isPreprocessProviderId(providerId)) {
return <DocPreprocessApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}
return <LlmApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}, [providerId, showHealthCheck])
return (
<Modal
@ -55,9 +51,7 @@ const PopupContainer: React.FC<Props> = ({ providerId, providerKind, title, reso
centered
width={600}
footer={null}>
{ListComponent && (
<ListComponent providerId={providerId} providerKind={providerKind} showHealthCheck={showHealthCheck} />
)}
{ListComponent}
</Modal>
)
}

View File

@ -8,6 +8,12 @@ export type ApiKeyValidity = {
error?: string
}
export type ApiProviderUnion = Provider | WebSearchProvider | PreprocessProvider
export type ApiProvider = Provider | WebSearchProvider | PreprocessProvider
export type ApiProviderKind = 'llm' | 'websearch' | 'doc-preprocess'
export type UpdateProviderFunc = (p: Partial<Provider>) => void
export type UpdateWebSearchProviderFunc = (p: Partial<WebSearchProvider>) => void
export type UpdatePreprocessProviderFunc = (p: Partial<PreprocessProvider>) => void
export type UpdateApiProviderFunc = UpdateProviderFunc | UpdateWebSearchProviderFunc | UpdatePreprocessProviderFunc

View File

@ -1,8 +1,9 @@
import Doc2xLogo from '@renderer/assets/images/ocr/doc2x.png'
import MinerULogo from '@renderer/assets/images/ocr/mineru.jpg'
import MistralLogo from '@renderer/assets/images/providers/mistral.png'
import { PreprocessProviderId } from '@renderer/types'
export function getPreprocessProviderLogo(providerId: string) {
export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
switch (providerId) {
case 'doc2x':
return Doc2xLogo
@ -15,7 +16,9 @@ export function getPreprocessProviderLogo(providerId: string) {
}
}
export const PREPROCESS_PROVIDER_CONFIG = {
type PreprocessProviderConfig = { websites: { official: string; apiKey: string } }
export const PREPROCESS_PROVIDER_CONFIG: Record<PreprocessProviderId, PreprocessProviderConfig> = {
doc2x: {
websites: {
official: 'https://doc2x.noedgeai.com',

View File

@ -1,24 +1,13 @@
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 { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
export function getWebSearchProviderLogo(providerId: string) {
switch (providerId) {
case 'tavily':
return TavilyLogo
case 'searxng':
return SearxngLogo
case 'exa':
return ExaLogo
case 'bocha':
return BochaLogo
default:
return undefined
type WebSearchProviderConfig = {
websites: {
official: string
apiKey?: string
}
}
export const WEB_SEARCH_PROVIDER_CONFIG = {
export const WEB_SEARCH_PROVIDER_CONFIG: Record<WebSearchProviderId, WebSearchProviderConfig> = {
tavily: {
websites: {
official: 'https://tavily.com',
@ -58,3 +47,46 @@ export const WEB_SEARCH_PROVIDER_CONFIG = {
}
}
}
export const WEB_SEARCH_PROVIDERS: WebSearchProvider[] = [
{
id: 'tavily',
name: 'Tavily',
apiHost: 'https://api.tavily.com',
apiKey: ''
},
{
id: 'searxng',
name: 'Searxng',
apiHost: '',
basicAuthUsername: '',
basicAuthPassword: ''
},
{
id: 'exa',
name: 'Exa',
apiHost: 'https://api.exa.ai',
apiKey: ''
},
{
id: 'bocha',
name: 'Bocha',
apiHost: 'https://api.bochaai.com',
apiKey: ''
},
{
id: 'local-google',
name: 'Google',
url: 'https://www.google.com/search?q=%s'
},
{
id: 'local-bing',
name: 'Bing',
url: 'https://cn.bing.com/search?q=%s&ensearch=1'
},
{
id: 'local-baidu',
name: 'Baidu',
url: 'https://www.baidu.com/s?wd=%s'
}
] as const

View File

@ -4,10 +4,10 @@ import {
updatePreprocessProvider as _updatePreprocessProvider,
updatePreprocessProviders as _updatePreprocessProviders
} from '@renderer/store/preprocess'
import { PreprocessProvider } from '@renderer/types'
import { PreprocessProvider, PreprocessProviderId } from '@renderer/types'
import { useDispatch, useSelector } from 'react-redux'
export const usePreprocessProvider = (id: string) => {
export const usePreprocessProvider = (id: PreprocessProviderId) => {
const dispatch = useDispatch()
const preprocessProviders = useSelector((state: RootState) => state.preprocess.providers)
const provider = preprocessProviders.find((provider) => provider.id === id)

View File

@ -11,7 +11,7 @@ import {
updateWebSearchProvider,
updateWebSearchProviders
} from '@renderer/store/websearch'
import { WebSearchProvider } from '@renderer/types'
import { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
export const useDefaultWebSearchProvider = () => {
const defaultProvider = useAppSelector((state) => state.websearch.defaultProvider)
@ -49,7 +49,7 @@ export const useWebSearchProviders = () => {
}
}
export const useWebSearchProvider = (id: string) => {
export const useWebSearchProvider = (id: WebSearchProviderId) => {
const providers = useAppSelector((state) => state.websearch.providers)
const provider = providers.find((provider) => provider.id === id)
const dispatch = useAppDispatch()
@ -60,7 +60,9 @@ export const useWebSearchProvider = (id: string) => {
return {
provider,
updateProvider: (updates: Partial<WebSearchProvider>) => dispatch(updateWebSearchProvider({ id, ...updates }))
updateProvider: (updates: Partial<WebSearchProvider>) => {
dispatch(updateWebSearchProvider({ id, ...updates }))
}
}
}

View File

@ -1,9 +1,11 @@
import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo } from '@renderer/components/Icons'
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { isWebSearchModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import WebSearchService from '@renderer/services/WebSearchService'
import { Assistant, WebSearchProvider } from '@renderer/types'
import { Assistant, WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Tooltip } from 'antd'
import { Globe } from 'lucide-react'
@ -28,6 +30,33 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch
const WebSearchIcon = useCallback(
({ pid, size = 18 }: { pid?: WebSearchProviderId; size?: number }) => {
const iconColor = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
switch (pid) {
case 'bocha':
return <BochaLogo width={size} height={size} color={iconColor} />
case 'exa':
// size微调视觉上和其他图标平衡一些
return <ExaLogo width={size - 2} height={size} color={iconColor} />
case 'tavily':
return <TavilyLogo width={size} height={size} color={iconColor} />
case 'searxng':
return <SearXNGLogo width={size} height={size} color={iconColor} />
case 'local-baidu':
return <BaiduOutlined size={size} style={{ color: iconColor, fontSize: size }} />
case 'local-bing':
return <BingLogo width={size} height={size} color={iconColor} />
case 'local-google':
return <GoogleOutlined size={size} style={{ color: iconColor, fontSize: size }} />
default:
return <Globe size={size} style={{ color: iconColor, fontSize: size }} />
}
},
[enableWebSearch]
)
const updateSelectedWebSearchProvider = useCallback(
async (providerId?: WebSearchProvider['id']) => {
// TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿
@ -58,7 +87,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
? t('settings.tool.websearch.apikey')
: t('settings.tool.websearch.free')
: t('chat.input.web_search.enable_content'),
icon: <Globe />,
icon: <WebSearchIcon size={13} pid={p.id} />,
isSelected: p.id === assistant?.webSearchProviderId,
disabled: !WebSearchService.isWebSearchEnabled(p.id),
action: () => updateSelectedWebSearchProvider(p.id)
@ -80,6 +109,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
return items
}, [
WebSearchIcon,
assistant.enableWebSearch,
assistant.model,
assistant?.webSearchProviderId,
@ -135,12 +165,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<Globe
size={18}
style={{
color: enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
}}
/>
<WebSearchIcon pid={assistant.webSearchProviderId} />
</ToolbarButton>
</Tooltip>
)

View File

@ -2,14 +2,14 @@ import { loggerService } from '@logger'
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { KnowledgeBase } from '@renderer/types'
import { KnowledgeBase, PreprocessProviderId } from '@renderer/types'
import { Tag } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('QuotaTag')
const QuotaTag: FC<{ base: KnowledgeBase; providerId: string; quota?: number }> = ({
const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quota?: number }> = ({
base,
providerId,
quota: _quota

View File

@ -4,23 +4,14 @@ import { getPreprocessProviderLogo, PREPROCESS_PROVIDER_CONFIG } from '@renderer
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import { PreprocessProvider } from '@renderer/types'
import { formatApiKeys, hasObjectKey } from '@renderer/utils'
import { Avatar, Button, Divider, Flex, Input, InputNumber, Segmented, Tooltip } from 'antd'
import { Avatar, Button, Divider, Flex, Input, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
import { 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,
SettingRow,
SettingRowTitle,
SettingSubtitle,
SettingTitle
} from '..'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
interface Props {
provider: PreprocessProvider
@ -31,7 +22,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
const { t } = useTranslation()
const [apiKey, setApiKey] = useState(preprocessProvider.apiKey || '')
const [apiHost, setApiHost] = useState(preprocessProvider.apiHost || '')
const [options, setOptions] = useState(preprocessProvider.options || {})
// const [options, setOptions] = useState(preprocessProvider.options || {})
const preprocessProviderConfig = PREPROCESS_PROVIDER_CONFIG[preprocessProvider.id]
const apiKeyWebsite = preprocessProviderConfig?.websites?.apiKey
@ -40,7 +31,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
useEffect(() => {
setApiKey(preprocessProvider.apiKey ?? '')
setApiHost(preprocessProvider.apiHost ?? '')
setOptions(preprocessProvider.options ?? {})
// setOptions(preprocessProvider.options ?? {})
}, [preprocessProvider.apiKey, preprocessProvider.apiHost, preprocessProvider.options])
const onUpdateApiKey = () => {
@ -52,7 +43,6 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
const openApiKeyList = async () => {
await ApiKeyListPopup.show({
providerId: preprocessProvider.id,
providerKind: 'doc-preprocess',
title: `${preprocessProvider.name} ${t('settings.provider.api.key.list.title')}`,
showHealthCheck: false // FIXME: 目前还没有检查功能
})
@ -70,11 +60,11 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
}
}
const onUpdateOptions = (key: string, value: any) => {
const newOptions = { ...options, [key]: value }
setOptions(newOptions)
updateProvider({ options: newOptions })
}
// const onUpdateOptions = (key: string, value: any) => {
// const newOptions = { ...options, [key]: value }
// setOptions(newOptions)
// updateProvider({ options: newOptions })
// }
return (
<>
@ -145,7 +135,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
)}
{/* 这部分看起来暂时用不上了 */}
{hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && (
{/* {hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && (
<>
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
<SettingRow>
@ -177,7 +167,7 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
/>
</SettingRow>
</>
)}
)} */}
</>
)
}

View File

@ -1,4 +1,3 @@
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultPreprocessProvider, usePreprocessProviders } from '@renderer/hooks/usePreprocess'
import { PreprocessProvider } from '@renderer/types'
@ -40,8 +39,9 @@ const PreprocessSettings: FC = () => {
placeholder={t('settings.tool.preprocess.provider_placeholder')}
options={preprocessProviders.map((p) => ({
value: p.id,
label: p.name,
disabled: !isMac && p.id === 'system' // 在非 Mac 系统下禁用 system 选项
label: p.name
// 由于system字段实际未使用先注释掉
// disabled: !isMac && p.id === 'system' // 在非 Mac 系统下禁用 system 选项
}))}
/>
</div>

View File

@ -128,7 +128,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const openApiKeyList = async () => {
await ApiKeyListPopup.show({
providerId: provider.id,
providerKind: 'llm',
title: `${fancyProviderName} ${t('settings.provider.api.key.list.title')}`
})
}

View File

@ -1,9 +1,14 @@
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 { getWebSearchProviderLogo, WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
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'
@ -16,7 +21,7 @@ import { SettingDivider, SettingHelpLink, SettingHelpText, SettingHelpTextRow, S
const logger = loggerService.withContext('WebSearchProviderSetting')
interface Props {
providerId: string
providerId: WebSearchProviderId
}
const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
@ -74,7 +79,6 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
const openApiKeyList = async () => {
await ApiKeyListPopup.show({
providerId: provider.id,
providerKind: 'websearch',
title: `${provider.name} ${t('settings.provider.api.key.list.title')}`
})
}
@ -132,6 +136,21 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
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>

View File

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { WEB_SEARCH_PROVIDERS } from '@renderer/config/webSearchProviders'
import type { Model, WebSearchProvider } from '@renderer/types'
export interface SubscribeSource {
key: number
@ -42,48 +43,7 @@ export interface WebSearchState {
export const initialState: WebSearchState = {
defaultProvider: 'local-bing',
providers: [
{
id: 'tavily',
name: 'Tavily',
apiHost: 'https://api.tavily.com',
apiKey: ''
},
{
id: 'searxng',
name: 'Searxng',
apiHost: '',
basicAuthUsername: '',
basicAuthPassword: ''
},
{
id: 'exa',
name: 'Exa',
apiHost: 'https://api.exa.ai',
apiKey: ''
},
{
id: 'bocha',
name: 'Bocha',
apiHost: 'https://api.bochaai.com',
apiKey: ''
},
{
id: 'local-google',
name: 'Google',
url: 'https://www.google.com/search?q=%s'
},
{
id: 'local-bing',
name: 'Bing',
url: 'https://cn.bing.com/search?q=%s&ensearch=1'
},
{
id: 'local-baidu',
name: 'Baidu',
url: 'https://www.baidu.com/s?wd=%s'
}
],
providers: WEB_SEARCH_PROVIDERS,
searchWithTime: true,
maxResults: 5,
excludeDomains: [],
@ -111,7 +71,7 @@ const websearchSlice = createSlice({
updateWebSearchProviders: (state, action: PayloadAction<WebSearchProvider[]>) => {
state.providers = action.payload
},
updateWebSearchProvider: (state, action: PayloadAction<Partial<WebSearchProvider> & { id: string }>) => {
updateWebSearchProvider: (state, action: PayloadAction<Partial<WebSearchProvider>>) => {
const index = state.providers.findIndex((provider) => provider.id === action.payload.id)
if (index !== -1) {
Object.assign(state.providers[index], action.payload)

View File

@ -609,8 +609,20 @@ export type KnowledgeBaseParams = {
}
}
export const PreprocessProviderIds = {
doc2x: 'doc2x',
mistral: 'mistral',
mineru: 'mineru'
} as const
export type PreprocessProviderId = keyof typeof PreprocessProviderIds
export const isPreprocessProviderId = (id: string): id is PreprocessProviderId => {
return Object.hasOwn(PreprocessProviderIds, id)
}
export interface PreprocessProvider {
id: string
id: PreprocessProviderId
name: string
apiKey?: string
apiHost?: string
@ -675,8 +687,24 @@ export type ExternalToolResult = {
memories?: MemoryItem[]
}
export const WebSearchProviderIds = {
tavily: 'tavily',
searxng: 'searxng',
exa: 'exa',
bocha: 'bocha',
'local-google': 'local-google',
'local-bing': 'local-bing',
'local-baidu': 'local-baidu'
} as const
export type WebSearchProviderId = keyof typeof WebSearchProviderIds
export const isWebSearchProviderId = (id: string): id is WebSearchProviderId => {
return Object.hasOwn(WebSearchProviderIds, id)
}
export type WebSearchProvider = {
id: string
id: WebSearchProviderId
name: string
apiKey?: string
apiHost?: string