diff --git a/package.json b/package.json index 54ed063e7a..d5c0338ddd 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "node-stream-zip": "^1.15.0", "officeparser": "^4.2.0", "os-proxy-config": "^1.1.2", - "selection-hook": "^1.0.8", + "selection-hook": "^1.0.9", "turndown": "7.2.0" }, "devDependencies": { diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index e60dac31f0..ea3b1f3f1e 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -9,6 +9,7 @@ import { CancellationToken, UpdateInfo } from 'builder-util-runtime' import { app, BrowserWindow, dialog } from 'electron' import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater' import path from 'path' +import semver from 'semver' import icon from '../../../build/icon.png?asset' import { configManager } from './ConfigManager' @@ -44,12 +45,6 @@ export default class AppUpdater { // 检测到不需要更新时 autoUpdater.on('update-not-available', () => { - if (configManager.getTestPlan() && this.autoUpdater.channel !== UpgradeChannel.LATEST) { - logger.info('test plan is enabled, but update is not available, do not send update not available event') - // will not send update not available event, because will check for updates with latest channel - return - } - windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateNotAvailable) }) @@ -72,18 +67,24 @@ export default class AppUpdater { this.autoUpdater = autoUpdater } - private async _getPreReleaseVersionFromGithub(channel: UpgradeChannel) { + private async _getReleaseVersionFromGithub(channel: UpgradeChannel) { + const headers = { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'Accept-Language': 'en-US,en;q=0.9' + } try { - logger.info(`get pre release version from github: ${channel}`) + logger.info(`get release version from github: ${channel}`) const responses = await fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', { - headers: { - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - 'Accept-Language': 'en-US,en;q=0.9' - } + headers }) const data = (await responses.json()) as GithubReleaseInfo[] + let mightHaveLatest = false const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => { + if (!item.draft && !item.prerelease) { + mightHaveLatest = true + } + return item.prerelease && item.tag_name.includes(`-${channel}.`) }) @@ -91,8 +92,29 @@ export default class AppUpdater { return null } - logger.info(`prerelease url is ${release.tag_name}, set channel to ${channel}`) + // if the release version is the same as the current version, return null + if (release.tag_name === app.getVersion()) { + return null + } + if (mightHaveLatest) { + logger.info(`might have latest release, get latest release`) + const latestReleaseResponse = await fetch( + 'https://api.github.com/repos/CherryHQ/cherry-studio/releases/latest', + { + headers + } + ) + const latestRelease = (await latestReleaseResponse.json()) as GithubReleaseInfo + if (semver.gt(latestRelease.tag_name, release.tag_name)) { + logger.info( + `latest release version is ${latestRelease.tag_name}, prerelease version is ${release.tag_name}, return null` + ) + return null + } + } + + logger.info(`release url is ${release.tag_name}, set channel to ${channel}`) return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}` } catch (error) { logger.error('Failed to get latest not draft version from github:', error as Error) @@ -151,14 +173,14 @@ export default class AppUpdater { return } - const preReleaseUrl = await this._getPreReleaseVersionFromGithub(channel) - if (preReleaseUrl) { - logger.info(`prerelease url is ${preReleaseUrl}, set channel to ${channel}`) - this._setChannel(channel, preReleaseUrl) + const releaseUrl = await this._getReleaseVersionFromGithub(channel) + if (releaseUrl) { + logger.info(`release url is ${releaseUrl}, set channel to ${channel}`) + this._setChannel(channel, releaseUrl) return } - // if no prerelease url, use github latest to avoid error + // if no prerelease url, use github latest to get release this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) return } @@ -195,17 +217,6 @@ export default class AppUpdater { `update check result: ${this.updateCheckResult?.isUpdateAvailable}, channel: ${this.autoUpdater.channel}, currentVersion: ${this.autoUpdater.currentVersion}` ) - // if the update is not available, and the test plan is enabled, set the feed url to the github latest - if ( - !this.updateCheckResult?.isUpdateAvailable && - configManager.getTestPlan() && - this.autoUpdater.channel !== UpgradeChannel.LATEST - ) { - logger.info('test plan is enabled, but update is not available, set channel to latest') - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) - this.updateCheckResult = await this.autoUpdater.checkForUpdates() - } - if (this.updateCheckResult?.isUpdateAvailable && !this.autoUpdater.autoDownload) { // 如果 autoDownload 为 false,则需要再调用下面的函数触发下 // do not use await, because it will block the return of this function diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index 56d3a97379..c6d3ee1841 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -21,6 +21,27 @@ class BackupManager { private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp') private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup') + // 缓存实例,避免重复创建 + private s3Storage: S3Storage | null = null + private webdavInstance: WebDav | null = null + + // 缓存核心连接配置,用于检测连接配置是否变更 + private cachedS3ConnectionConfig: { + endpoint: string + region: string + bucket: string + accessKeyId: string + secretAccessKey: string + root?: string + } | null = null + + private cachedWebdavConnectionConfig: { + webdavHost: string + webdavUser?: string + webdavPass?: string + webdavPath?: string + } | null = null + constructor() { this.checkConnection = this.checkConnection.bind(this) this.backup = this.backup.bind(this) @@ -87,6 +108,88 @@ class BackupManager { } } + /** + * 比较两个配置对象是否相等,只比较影响客户端连接的核心字段,忽略 fileName 等易变字段 + */ + private isS3ConfigEqual(cachedConfig: typeof this.cachedS3ConnectionConfig, config: S3Config): boolean { + if (!cachedConfig) return false + + return ( + cachedConfig.endpoint === config.endpoint && + cachedConfig.region === config.region && + cachedConfig.bucket === config.bucket && + cachedConfig.accessKeyId === config.accessKeyId && + cachedConfig.secretAccessKey === config.secretAccessKey && + cachedConfig.root === config.root + ) + } + + /** + * 深度比较两个 WebDAV 配置对象是否相等,只比较影响客户端连接的核心字段,忽略 fileName 等易变字段 + */ + private isWebDavConfigEqual(cachedConfig: typeof this.cachedWebdavConnectionConfig, config: WebDavConfig): boolean { + if (!cachedConfig) return false + + return ( + cachedConfig.webdavHost === config.webdavHost && + cachedConfig.webdavUser === config.webdavUser && + cachedConfig.webdavPass === config.webdavPass && + cachedConfig.webdavPath === config.webdavPath + ) + } + + /** + * 获取 S3Storage 实例,如果连接配置未变且实例已存在则复用,否则创建新实例 + * 注意:只有连接相关的配置变更才会重新创建实例,其他配置变更不影响实例复用 + */ + private getS3Storage(config: S3Config): S3Storage { + // 检查核心连接配置是否变更 + const configChanged = !this.isS3ConfigEqual(this.cachedS3ConnectionConfig, config) + + if (configChanged || !this.s3Storage) { + this.s3Storage = new S3Storage(config) + // 只缓存连接相关的配置字段 + this.cachedS3ConnectionConfig = { + endpoint: config.endpoint, + region: config.region, + bucket: config.bucket, + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + root: config.root + } + logger.debug('[BackupManager] Created new S3Storage instance') + } else { + logger.debug('[BackupManager] Reusing existing S3Storage instance') + } + + return this.s3Storage + } + + /** + * 获取 WebDav 实例,如果连接配置未变且实例已存在则复用,否则创建新实例 + * 注意:只有连接相关的配置变更才会重新创建实例,其他配置变更不影响实例复用 + */ + private getWebDavInstance(config: WebDavConfig): WebDav { + // 检查核心连接配置是否变更 + const configChanged = !this.isWebDavConfigEqual(this.cachedWebdavConnectionConfig, config) + + if (configChanged || !this.webdavInstance) { + this.webdavInstance = new WebDav(config) + // 只缓存连接相关的配置字段 + this.cachedWebdavConnectionConfig = { + webdavHost: config.webdavHost, + webdavUser: config.webdavUser, + webdavPass: config.webdavPass, + webdavPath: config.webdavPath + } + logger.debug('[BackupManager] Created new WebDav instance') + } else { + logger.debug('[BackupManager] Reusing existing WebDav instance') + } + + return this.webdavInstance + } + async backup( _: Electron.IpcMainInvokeEvent, fileName: string, @@ -322,7 +425,7 @@ class BackupManager { async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) { const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile) - const webdavClient = new WebDav(webdavConfig) + const webdavClient = this.getWebDavInstance(webdavConfig) try { let result if (webdavConfig.disableStream) { @@ -349,7 +452,7 @@ class BackupManager { async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) { const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' - const webdavClient = new WebDav(webdavConfig) + const webdavClient = this.getWebDavInstance(webdavConfig) try { const retrievedFile = await webdavClient.getFileContents(filename) const backupedFilePath = path.join(this.backupDir, filename) @@ -377,7 +480,7 @@ class BackupManager { listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => { try { - const client = new WebDav(config) + const client = this.getWebDavInstance(config) const response = await client.getDirectoryContents() const files = Array.isArray(response) ? response : response.data @@ -467,7 +570,7 @@ class BackupManager { } async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) { - const webdavClient = new WebDav(webdavConfig) + const webdavClient = this.getWebDavInstance(webdavConfig) return await webdavClient.checkConnection() } @@ -477,13 +580,13 @@ class BackupManager { path: string, options?: CreateDirectoryOptions ) { - const webdavClient = new WebDav(webdavConfig) + const webdavClient = this.getWebDavInstance(webdavConfig) return await webdavClient.createDirectory(path, options) } async deleteWebdavFile(_: Electron.IpcMainInvokeEvent, fileName: string, webdavConfig: WebDavConfig) { try { - const webdavClient = new WebDav(webdavConfig) + const webdavClient = this.getWebDavInstance(webdavConfig) return await webdavClient.deleteFile(fileName) } catch (error: any) { logger.error('Failed to delete WebDAV file:', error) @@ -525,7 +628,7 @@ class BackupManager { logger.debug(`Starting S3 backup to ${filename}`) const backupedFilePath = await this.backup(_, filename, data, undefined, s3Config.skipBackupFile) - const s3Client = new S3Storage(s3Config) + const s3Client = this.getS3Storage(s3Config) try { const fileBuffer = await fs.promises.readFile(backupedFilePath) const result = await s3Client.putFileContents(filename, fileBuffer) @@ -603,7 +706,7 @@ class BackupManager { logger.debug(`Starting restore from S3: ${filename}`) - const s3Client = new S3Storage(s3Config) + const s3Client = this.getS3Storage(s3Config) try { const retrievedFile = await s3Client.getFileContents(filename) const backupedFilePath = path.join(this.backupDir, filename) @@ -628,7 +731,7 @@ class BackupManager { listS3Files = async (_: Electron.IpcMainInvokeEvent, s3Config: S3Config) => { try { - const s3Client = new S3Storage(s3Config) + const s3Client = this.getS3Storage(s3Config) const objects = await s3Client.listFiles() const files = objects @@ -652,7 +755,7 @@ class BackupManager { async deleteS3File(_: Electron.IpcMainInvokeEvent, fileName: string, s3Config: S3Config) { try { - const s3Client = new S3Storage(s3Config) + const s3Client = this.getS3Storage(s3Config) return await s3Client.deleteFile(fileName) } catch (error: any) { logger.error('Failed to delete S3 file:', error) @@ -661,7 +764,7 @@ class BackupManager { } async checkS3Connection(_: Electron.IpcMainInvokeEvent, s3Config: S3Config) { - const s3Client = new S3Storage(s3Config) + const s3Client = this.getS3Storage(s3Config) return await s3Client.checkConnection() } } diff --git a/src/renderer/src/components/Icons/SVGIcon.tsx b/src/renderer/src/components/Icons/SVGIcon.tsx index b9a3eff899..88598bb02e 100644 --- a/src/renderer/src/components/Icons/SVGIcon.tsx +++ b/src/renderer/src/components/Icons/SVGIcon.tsx @@ -112,3 +112,129 @@ export function MdiLightbulbOn(props: SVGProps) { ) } + +export function BingLogo(props: SVGProps) { + return ( + + + + ) +} + +export function SearXNGLogo(props: SVGProps) { + return ( + + + + + + + + ) +} + +export function TavilyLogo(props: SVGProps) { + return ( + + + + + + + + + ) +} + +export function ExaLogo(props: SVGProps) { + return ( + + Exa + + + ) +} + +export function BochaLogo(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts b/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts index 541dd2f156..c4c9459ed0 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts @@ -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) => 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) } // 获取模型用于检查 diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/list.tsx b/src/renderer/src/components/Popups/ApiKeyListPopup/list.tsx index 03e9796886..86076b4ca8 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/list.tsx +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/list.tsx @@ -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) => void - providerKind: ApiProviderKind + provider: ApiProvider + updateProvider: UpdateApiProviderFunc showHealthCheck?: boolean } /** * Api key 列表,管理 CRUD 操作、连接检查 */ -export const ApiKeyList: FC = ({ provider, updateProvider, providerKind, showHealthCheck = true }) => { +export const ApiKeyList: FC = ({ provider, updateProvider, showHealthCheck = true }) => { const { t } = useTranslation() // 临时新项状态 @@ -42,7 +42,7 @@ export const ApiKeyList: FC = ({ provider, updateProvider, prov checkKeyConnectivity, checkAllKeysConnectivity, isChecking - } = useApiKeys({ provider, updateProvider, providerKind: providerKind }) + } = useApiKeys({ provider, updateProvider }) // 创建一个临时新项 const handleAddNew = () => { @@ -73,7 +73,7 @@ export const ApiKeyList: FC = ({ 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 = ({ provider, updateProvider, prov interface SpecificApiKeyListProps { providerId: string - providerKind: ApiProviderKind showHealthCheck?: boolean } -export const LlmApiKeyList: FC = ({ providerId, providerKind, showHealthCheck = true }) => { +type WebSearchApiKeyList = SpecificApiKeyListProps & { + providerId: WebSearchProviderId +} + +type DocPreprocessApiKeyListProps = SpecificApiKeyListProps & { + providerId: PreprocessProviderId +} + +export const LlmApiKeyList: FC = ({ providerId, showHealthCheck = true }) => { const { provider, updateProvider } = useProvider(providerId) - return ( - - ) + return } -export const WebSearchApiKeyList: FC = ({ - providerId, - providerKind, - showHealthCheck = true -}) => { +export const WebSearchApiKeyList: FC = ({ providerId, showHealthCheck = true }) => { const { provider, updateProvider } = useWebSearchProvider(providerId) - return ( - - ) + return } -export const DocPreprocessApiKeyList: FC = ({ - providerId, - providerKind, - showHealthCheck = true -}) => { +export const DocPreprocessApiKeyList: FC = ({ providerId, showHealthCheck = true }) => { const { provider, updateProvider } = usePreprocessProvider(providerId) - return ( - - ) + return } const ListContainer = styled.div` diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/popup.tsx b/src/renderer/src/components/Popups/ApiKeyListPopup/popup.tsx index 096e00ca58..b4ca91186b 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/popup.tsx +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/popup.tsx @@ -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 = ({ providerId, providerKind, title, resolve, showHealthCheck = true }) => { +const PopupContainer: React.FC = ({ providerId, title, resolve, showHealthCheck = true }) => { const [open, setOpen] = useState(true) const { t } = useTranslation() @@ -33,17 +32,14 @@ const PopupContainer: React.FC = ({ 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 } - }, [providerKind]) + if (isPreprocessProviderId(providerId)) { + return + } + return + }, [providerId, showHealthCheck]) return ( = ({ providerId, providerKind, title, reso centered width={600} footer={null}> - {ListComponent && ( - - )} + {ListComponent} ) } diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts b/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts index 4663e70715..bc230c577d 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts @@ -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) => void + +export type UpdateWebSearchProviderFunc = (p: Partial) => void + +export type UpdatePreprocessProviderFunc = (p: Partial) => void + +export type UpdateApiProviderFunc = UpdateProviderFunc | UpdateWebSearchProviderFunc | UpdatePreprocessProviderFunc diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index eef1044fb3..35e6872e86 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -2724,7 +2724,7 @@ export function isSupportedThinkingTokenDoubaoModel(model?: Model): boolean { const modelId = getLowerBaseModelName(model.id, '/') - return DOUBAO_THINKING_MODEL_REGEX.test(modelId) || DOUBAO_THINKING_MODEL_REGEX.test(modelId) + return DOUBAO_THINKING_MODEL_REGEX.test(modelId) || DOUBAO_THINKING_MODEL_REGEX.test(model.name) } export function isClaudeReasoningModel(model?: Model): boolean { diff --git a/src/renderer/src/config/preprocessProviders.ts b/src/renderer/src/config/preprocessProviders.ts index 587e6ea7f9..88215b328d 100644 --- a/src/renderer/src/config/preprocessProviders.ts +++ b/src/renderer/src/config/preprocessProviders.ts @@ -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 = { doc2x: { websites: { official: 'https://doc2x.noedgeai.com', diff --git a/src/renderer/src/config/webSearchProviders.ts b/src/renderer/src/config/webSearchProviders.ts index d33e9cba35..62c0536f4d 100644 --- a/src/renderer/src/config/webSearchProviders.ts +++ b/src/renderer/src/config/webSearchProviders.ts @@ -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 = { 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 diff --git a/src/renderer/src/hooks/usePreprocess.ts b/src/renderer/src/hooks/usePreprocess.ts index 41463227ad..5172e2c68a 100644 --- a/src/renderer/src/hooks/usePreprocess.ts +++ b/src/renderer/src/hooks/usePreprocess.ts @@ -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) diff --git a/src/renderer/src/hooks/useWebSearchProviders.ts b/src/renderer/src/hooks/useWebSearchProviders.ts index 32f9238abf..34ee07403e 100644 --- a/src/renderer/src/hooks/useWebSearchProviders.ts +++ b/src/renderer/src/hooks/useWebSearchProviders.ts @@ -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) => dispatch(updateWebSearchProvider({ id, ...updates })) + updateProvider: (updates: Partial) => { + dispatch(updateWebSearchProvider({ id, ...updates })) + } } } diff --git a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx index 7a6a6e6334..70b25084d5 100644 --- a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx @@ -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 = ({ 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 + case 'exa': + // size微调,视觉上和其他图标平衡一些 + return + case 'tavily': + return + case 'searxng': + return + case 'local-baidu': + return + case 'local-bing': + return + case 'local-google': + return + default: + return + } + }, + [enableWebSearch] + ) + const updateSelectedWebSearchProvider = useCallback( async (providerId?: WebSearchProvider['id']) => { // TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿 @@ -58,7 +87,7 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { ? t('settings.tool.websearch.apikey') : t('settings.tool.websearch.free') : t('chat.input.web_search.enable_content'), - icon: , + icon: , isSelected: p.id === assistant?.webSearchProviderId, disabled: !WebSearchService.isWebSearchEnabled(p.id), action: () => updateSelectedWebSearchProvider(p.id) @@ -80,6 +109,7 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { return items }, [ + WebSearchIcon, assistant.enableWebSearch, assistant.model, assistant?.webSearchProviderId, @@ -135,12 +165,7 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { mouseLeaveDelay={0} arrow> - + ) diff --git a/src/renderer/src/pages/knowledge/components/QuotaTag.tsx b/src/renderer/src/pages/knowledge/components/QuotaTag.tsx index 8eb8868e98..b6f29ce5d1 100644 --- a/src/renderer/src/pages/knowledge/components/QuotaTag.tsx +++ b/src/renderer/src/pages/knowledge/components/QuotaTag.tsx @@ -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 diff --git a/src/renderer/src/pages/settings/PreprocessSettings/PreprocessSettings.tsx b/src/renderer/src/pages/settings/PreprocessSettings/PreprocessSettings.tsx index c1592ac73f..d19eec4d6d 100644 --- a/src/renderer/src/pages/settings/PreprocessSettings/PreprocessSettings.tsx +++ b/src/renderer/src/pages/settings/PreprocessSettings/PreprocessSettings.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ provider: _provider }) => { )} {/* 这部分看起来暂时用不上了 */} - {hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && ( + {/* {hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && ( <> @@ -177,7 +167,7 @@ const PreprocessProviderSettings: FC = ({ provider: _provider }) => { /> - )} + )} */} ) } diff --git a/src/renderer/src/pages/settings/PreprocessSettings/index.tsx b/src/renderer/src/pages/settings/PreprocessSettings/index.tsx index daa76c042c..f80c0cd679 100644 --- a/src/renderer/src/pages/settings/PreprocessSettings/index.tsx +++ b/src/renderer/src/pages/settings/PreprocessSettings/index.tsx @@ -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 选项 }))} /> diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index b82acaa511..191cce4778 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -128,7 +128,6 @@ const ProviderSetting: FC = ({ providerId }) => { const openApiKeyList = async () => { await ApiKeyListPopup.show({ providerId: provider.id, - providerKind: 'llm', title: `${fancyProviderName} ${t('settings.provider.api.key.list.title')}` }) } diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx index 68bb689bd6..5c6faa7b78 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx @@ -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 = ({ providerId }) => { @@ -74,7 +79,6 @@ const WebSearchProviderSetting: FC = ({ 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 = ({ 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 ( <> diff --git a/src/renderer/src/services/messageStreaming/callbacks/citationCallbacks.ts b/src/renderer/src/services/messageStreaming/callbacks/citationCallbacks.ts index 56d0680839..9ba743b2cd 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/citationCallbacks.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/citationCallbacks.ts @@ -40,6 +40,7 @@ export const createCitationCallbacks = (deps: CitationCallbacksDependencies) => status: MessageBlockStatus.SUCCESS } blockManager.smartBlockUpdate(citationBlockId, changes, MessageBlockType.CITATION, true) + citationBlockId = null } else { logger.error('[onExternalToolComplete] citationBlockId is null. Cannot update.') } diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index d7f0799814..1e3fe2a25b 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -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) => { state.providers = action.payload }, - updateWebSearchProvider: (state, action: PayloadAction & { id: string }>) => { + updateWebSearchProvider: (state, action: PayloadAction>) => { const index = state.providers.findIndex((provider) => provider.id === action.payload.id) if (index !== -1) { Object.assign(state.providers[index], action.payload) diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 438213b097..af42ded544 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -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 diff --git a/yarn.lock b/yarn.lock index 762bfd466f..03c6fe7dbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -75,10 +75,10 @@ __metadata: linkType: hard "@ai-sdk/amazon-bedrock@npm:^3.0.0": - version: 3.0.7 - resolution: "@ai-sdk/amazon-bedrock@npm:3.0.7" + version: 3.0.8 + resolution: "@ai-sdk/amazon-bedrock@npm:3.0.8" dependencies: - "@ai-sdk/anthropic": "npm:2.0.3" + "@ai-sdk/anthropic": "npm:2.0.4" "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.3" "@smithy/eventstream-codec": "npm:^4.0.1" @@ -86,7 +86,7 @@ __metadata: aws4fetch: "npm:^1.0.20" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/96d118413db091fd325edc0a0900a7376ddeecb63de9d0bd00eca5f0f8ac86185eafe28740b19829a6cd873b304cc6ba6e3313bc435fdc6156abf9b705db1af4 + checksum: 10c0/d7b303b8581e9d28e9ac375b3718ef3f7fff3353d18185870f0b90fd542eb9398d029768502981e9e45a6b64137a7029f591993afd0b18e9ef74525f625524f7 languageName: node linkType: hard @@ -102,15 +102,15 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/anthropic@npm:2.0.3": - version: 2.0.3 - resolution: "@ai-sdk/anthropic@npm:2.0.3" +"@ai-sdk/anthropic@npm:2.0.4": + version: 2.0.4 + resolution: "@ai-sdk/anthropic@npm:2.0.4" dependencies: "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.3" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/733861197ae6c7fe93e9dc131a2176bd654eeee0b0575bb164f4259ecdb23d85039a0ef3ba4b1ad2de299c275b97d0fb1b0be0cff224713e81d0a31749ae1070 + checksum: 10c0/2e5a997b6e2d9a2964c4681418643fd2f347df78ac1f9677a0cc6a3a3454920d05c663e35521d8922f0a382ec77a25e4b92204b3760a1da05876bf00d41adc39 languageName: node linkType: hard @@ -153,17 +153,17 @@ __metadata: linkType: hard "@ai-sdk/google-vertex@npm:^3.0.0": - version: 3.0.8 - resolution: "@ai-sdk/google-vertex@npm:3.0.8" + version: 3.0.9 + resolution: "@ai-sdk/google-vertex@npm:3.0.9" dependencies: - "@ai-sdk/anthropic": "npm:2.0.3" + "@ai-sdk/anthropic": "npm:2.0.4" "@ai-sdk/google": "npm:2.0.6" "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.3" google-auth-library: "npm:^9.15.0" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/3d0b89f64fcd9eafa7a1af5f21cf317fa3c3d2ca1fae55bc5ab22e969ddc3f6db6cb918b9bb27ec5b3f32eca518e0529877db2826abfe8fce64426bda3e0775f + checksum: 10c0/c6584b877f9e20a10dd7d92fc4cb1b4a9838510aa89734cf1ff2faa74ba820b976d3359d4eadcb6035c8911973300efb157931fa0d1105abc8db36f94544cc88 languageName: node linkType: hard @@ -1285,38 +1285,38 @@ __metadata: linkType: hard "@babel/core@npm:^7.27.7": - version: 7.28.0 - resolution: "@babel/core@npm:7.28.0" + version: 7.28.3 + resolution: "@babel/core@npm:7.28.3" dependencies: "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.3" "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.6" - "@babel/parser": "npm:^7.28.0" + "@babel/helper-module-transforms": "npm:^7.28.3" + "@babel/helpers": "npm:^7.28.3" + "@babel/parser": "npm:^7.28.3" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 + checksum: 10c0/e6b3eb830c4b93f5a442b305776df1cd2bb4fafa4612355366f67c764f3e54a69d45b84def77fb2d4fd83439102667b0a92c3ea2838f678733245b748c602a7b languageName: node linkType: hard -"@babel/generator@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/generator@npm:7.28.0" +"@babel/generator@npm:^7.28.0, @babel/generator@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/generator@npm:7.28.3" dependencies: - "@babel/parser": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" "@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92 + checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc languageName: node linkType: hard @@ -1350,16 +1350,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/helper-module-transforms@npm:7.27.3" +"@babel/helper-module-transforms@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helper-module-transforms@npm:7.28.3" dependencies: "@babel/helper-module-imports": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.3" + "@babel/traverse": "npm:^7.28.3" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/fccb4f512a13b4c069af51e1b56b20f54024bcf1591e31e978a30f3502567f34f90a80da6a19a6148c249216292a8074a0121f9e52602510ef0f32dbce95ca01 + checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb languageName: node linkType: hard @@ -1391,24 +1391,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.6": - version: 7.28.2 - resolution: "@babel/helpers@npm:7.28.2" +"@babel/helpers@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helpers@npm:7.28.3" dependencies: "@babel/template": "npm:^7.27.2" "@babel/types": "npm:^7.28.2" - checksum: 10c0/f3e7b21517e2699c4ca193663ecfb1bf1b2ae2762d8ba4a9f1786feaca0d6984537fc60bf2206e92c43640a6dada6b438f523cc1ad78610d0151aeb061b37f63 + checksum: 10c0/03a8f94135415eec62d37be9c62c63908f2d5386c7b00e04545de4961996465775330e3eb57717ea7451e19b0e24615777ebfec408c2adb1df3b10b4df6bf1ce languageName: node linkType: hard -"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/parser@npm:7.28.0" +"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0, @babel/parser@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/parser@npm:7.28.3" dependencies: - "@babel/types": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" bin: parser: ./bin/babel-parser.js - checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5 + checksum: 10c0/1f41eb82623b0ca0f94521b57f4790c6c457cd922b8e2597985b36bdec24114a9ccf54640286a760ceb60f11fe9102d192bf60477aee77f5d45f1029b9b72729 languageName: node linkType: hard @@ -1424,9 +1424,9 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.28.2 - resolution: "@babel/runtime@npm:7.28.2" - checksum: 10c0/c20afe253629d53a405a610b12a62ac74d341a2c1e0fb202bbef0c118f6b5c84f94bf16039f58fd0483dd256901259930a43976845bdeb180cab1f882c21b6e0 + version: 7.28.3 + resolution: "@babel/runtime@npm:7.28.3" + checksum: 10c0/b360f82c2c5114f2a062d4d143d7b4ec690094764853937110585a9497977aed66c102166d0e404766c274e02a50ffb8f6d77fef7251ecf3f607f0e03e6397bc languageName: node linkType: hard @@ -1441,22 +1441,22 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/traverse@npm:7.28.0" +"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/traverse@npm:7.28.3" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.3" "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.3" "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" debug: "npm:^4.3.1" - checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9 + checksum: 10c0/26e95b29a46925b7b41255e03185b7e65b2c4987e14bbee7bbf95867fb19c69181f301bbe1c7b201d4fe0cce6aa0cbea0282dad74b3a0fef3d9058f6c76fdcb3 languageName: node linkType: hard -"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0, @babel/types@npm:^7.28.1, @babel/types@npm:^7.28.2": +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.1, @babel/types@npm:^7.28.2": version: 7.28.2 resolution: "@babel/types@npm:7.28.2" dependencies: @@ -3973,8 +3973,8 @@ __metadata: linkType: hard "@modelcontextprotocol/sdk@npm:^1.17.0": - version: 1.17.2 - resolution: "@modelcontextprotocol/sdk@npm:1.17.2" + version: 1.17.3 + resolution: "@modelcontextprotocol/sdk@npm:1.17.3" dependencies: ajv: "npm:^6.12.6" content-type: "npm:^1.0.5" @@ -3988,7 +3988,7 @@ __metadata: raw-body: "npm:^3.0.0" zod: "npm:^3.23.8" zod-to-json-schema: "npm:^3.24.1" - checksum: 10c0/9f0d00dc5f96a6bc78b775d2ce57d72270cf3b0fad08209f0061e30fff6e2005b7e6abbef98fbc030ba7e3aa5c78e5211f9b5c2a3d0d8f4455dfd47fef228720 + checksum: 10c0/23df0949724279eaa620f2780e3c731dcf746311f3175e3cba602643aacf9ee6dbcf99daeab3fa848296fe9ac971456fc031c79c1b55870dd019baf0263fd080 languageName: node linkType: hard @@ -5927,21 +5927,21 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.85.2": - version: 5.85.2 - resolution: "@tanstack/query-core@npm:5.85.2" - checksum: 10c0/398386c0e56b0ab0916951468b6b6a2b9f1f713fa95757405171f1c99c7a33cd3427eb20a82f252e5abc63ccd93e8e77c7cc5bc30043401d0dd5a074d7f5ff4e +"@tanstack/query-core@npm:5.85.3": + version: 5.85.3 + resolution: "@tanstack/query-core@npm:5.85.3" + checksum: 10c0/7db9e78c3648a3d5bc295fff14e7afdf061e38ca55b0004e3f6e6f1f44596f564bf8b59c97b2d1f7ce851792c8eac7008608ccd4de0dfcd6349a4e0943b7d247 languageName: node linkType: hard "@tanstack/react-query@npm:^5.27.0": - version: 5.85.2 - resolution: "@tanstack/react-query@npm:5.85.2" + version: 5.85.3 + resolution: "@tanstack/react-query@npm:5.85.3" dependencies: - "@tanstack/query-core": "npm:5.85.2" + "@tanstack/query-core": "npm:5.85.3" peerDependencies: react: ^18 || ^19 - checksum: 10c0/8feb9a82d25589b7f3a9cc1e3f24ec2b29b35e86522ca94b3f94dc2649e4a4472da7782b2e1f0b7a48722d11e8c34cf77711b9fbad474b1b73478ae0f06c9ba0 + checksum: 10c0/3588a6997c5f5302d999f98230894ad3ed5d24b0b063b62c34300e2cd3a715afc7edd97eb8a99408f2f7f85e76812fa87d035dbbc340c95adecade80d1e44ae0 languageName: node linkType: hard @@ -7871,7 +7871,7 @@ __metadata: remove-markdown: "npm:^0.6.2" rollup-plugin-visualizer: "npm:^5.12.0" sass: "npm:^1.88.0" - selection-hook: "npm:^1.0.8" + selection-hook: "npm:^1.0.9" shiki: "npm:^3.9.1" strict-url-sanitise: "npm:^0.0.1" string-width: "npm:^7.2.0" @@ -11001,9 +11001,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.199": - version: 1.5.200 - resolution: "electron-to-chromium@npm:1.5.200" - checksum: 10c0/3f323c097d41c31da09632df30fa26a887c4805fb3a12196eaededc4337cc3b62530d2c80d18ed352c7732c8edfadefbc604ccefe6d4e83c1156c2ed1525bdee + version: 1.5.201 + resolution: "electron-to-chromium@npm:1.5.201" + checksum: 10c0/83f415506e4f79ebe3bcf311526823fe73cd9d54938eff6d98504d222d7c1f7db21acbe98b30394349b1dc973ac85123f1afb5f2b495e166885e5ddd7cf086e6 languageName: node linkType: hard @@ -12111,14 +12111,14 @@ __metadata: linkType: hard "fdir@npm:^6.4.4, fdir@npm:^6.4.6": - version: 6.4.6 - resolution: "fdir@npm:6.4.6" + version: 6.5.0 + resolution: "fdir@npm:6.5.0" peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true - checksum: 10c0/45b559cff889934ebb8bc498351e5acba40750ada7e7d6bde197768d2fa67c149be8ae7f8ff34d03f4e1eb20f2764116e56440aaa2f6689e9a4aa7ef06acafe9 + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f languageName: node linkType: hard @@ -14398,8 +14398,8 @@ __metadata: linkType: hard "langsmith@npm:>=0.2.8 <0.4.0, langsmith@npm:^0.3.33, langsmith@npm:^0.3.46": - version: 0.3.59 - resolution: "langsmith@npm:0.3.59" + version: 0.3.61 + resolution: "langsmith@npm:0.3.61" dependencies: "@types/uuid": "npm:^10.0.0" chalk: "npm:^4.1.2" @@ -14422,7 +14422,7 @@ __metadata: optional: true openai: optional: true - checksum: 10c0/e6c8c1f9f88a76116efa7fbb1af38be266759eb4e9a4c498e80e2d6cf5b456bbf0e607077cd14e0d864234685a0553b67f96bbc6b05f985b797cc41da8d6a682 + checksum: 10c0/2aaf611bf8f7b2e44d9266415a9933b5f94986e02a2fa89be560b9118b118f9c8722d1a3c677f1bbeac17d3330706e7bbfeeeaa814587de89d8a18fd97c0a45b languageName: node linkType: hard @@ -19795,7 +19795,7 @@ __metadata: languageName: node linkType: hard -"selection-hook@npm:^1.0.8": +"selection-hook@npm:^1.0.9": version: 1.0.9 resolution: "selection-hook@npm:1.0.9" dependencies: