feat: Enhance API key verification and multi-key support for web search providers (#3346)
* feat(websearch): implement API key formatting and add WebSearchApiCheckPopup for multiple keys validation - Introduced a new WebSearchApiCheckPopup component to validate multiple API keys. - Added formatApiKeys function to standardize API key input. - Updated WebSearchProviderSetting to utilize the new popup for checking multiple keys. - Enhanced error handling and user feedback for API key validation. * feat(settings): enhance API key validation for providers and web search - Updated ApiCheckPopup to handle both provider and web search API key validation. - Refactored key checking logic to differentiate between provider and web search types. - Removed the obsolete WebSearchApiCheckPopup component and integrated its functionality into ApiCheckPopup. - Adjusted WebSearchProviderSetting to utilize the updated ApiCheckPopup for checking multiple keys.
This commit is contained in:
parent
4c5b8ee0ee
commit
aa6ecb4814
@ -967,6 +967,7 @@
|
||||
"blacklist_description": "Results from the following websites will not appear in search results",
|
||||
"blacklist_tooltip": "Please use the following format (separated by line breaks)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "Check",
|
||||
"check_success": "Verification successful",
|
||||
"check_failed": "Verification failed",
|
||||
"get_api_key": "Get API Key",
|
||||
"no_provider_selected": "Please select a search service provider before checking.",
|
||||
|
||||
@ -967,6 +967,7 @@
|
||||
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
|
||||
"blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "チェック",
|
||||
"check_success": "検証に成功しました",
|
||||
"check_failed": "検証に失敗しました",
|
||||
"get_api_key": "APIキーを取得",
|
||||
"no_provider_selected": "検索サービスプロバイダーを選択してから再確認してください。",
|
||||
|
||||
@ -967,6 +967,7 @@
|
||||
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
|
||||
"blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "проверка",
|
||||
"check_success": "Проверка успешна",
|
||||
"check_failed": "Проверка не прошла",
|
||||
"get_api_key": "Получить ключ API",
|
||||
"no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.",
|
||||
|
||||
@ -967,6 +967,7 @@
|
||||
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
|
||||
"blacklist_tooltip": "请使用以下格式(换行分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "检查",
|
||||
"check_success": "验证成功",
|
||||
"check_failed": "验证失败",
|
||||
"get_api_key": "点击这里获取密钥",
|
||||
"no_provider_selected": "请选择搜索服务商后再检查",
|
||||
|
||||
@ -967,6 +967,7 @@
|
||||
"blacklist_description": "以下網站不會出現在搜尋結果中",
|
||||
"blacklist_tooltip": "請使用以下格式 (換行符號分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "檢查",
|
||||
"check_success": "驗證成功",
|
||||
"check_failed": "驗證失敗",
|
||||
"get_api_key": "點選這裡取得金鑰",
|
||||
"no_provider_selected": "請選擇搜尋服務商後再檢查",
|
||||
|
||||
@ -2,8 +2,8 @@ import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, MinusCircleOutli
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { checkApi } from '@renderer/services/ApiService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Provider } from '@renderer/types'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { Model, Provider, WebSearchProvider } from '@renderer/types'
|
||||
import { maskApiKey } from '@renderer/utils/api'
|
||||
import { Button, List, Modal, Space, Spin, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
@ -12,9 +12,10 @@ import styled from 'styled-components'
|
||||
|
||||
interface ShowParams {
|
||||
title: string
|
||||
provider: Provider
|
||||
model: Model
|
||||
provider: Provider | WebSearchProvider
|
||||
model?: Model
|
||||
apiKeys: string[]
|
||||
type: 'provider' | 'websearch'
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
@ -27,7 +28,7 @@ interface KeyStatus {
|
||||
checking?: boolean
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, resolve }) => {
|
||||
const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, type, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [keyStatuses, setKeyStatuses] = useState<KeyStatus[]>(() => {
|
||||
const uniqueKeys = new Set(apiKeys)
|
||||
@ -45,7 +46,17 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
||||
for (let i = 0; i < newStatuses.length; i++) {
|
||||
setKeyStatuses((prev) => prev.map((status, idx) => (idx === i ? { ...status, checking: true } : status)))
|
||||
|
||||
const { valid } = await checkApi({ ...provider, apiKey: newStatuses[i].key }, model)
|
||||
let valid = false
|
||||
if (type === 'provider' && model) {
|
||||
const result = await checkApi({ ...(provider as Provider), apiKey: newStatuses[i].key }, model)
|
||||
valid = result.valid
|
||||
} else {
|
||||
const result = await WebSearchService.checkSearch({
|
||||
...(provider as WebSearchProvider),
|
||||
apiKey: newStatuses[i].key
|
||||
})
|
||||
valid = result.valid
|
||||
}
|
||||
|
||||
setKeyStatuses((prev) =>
|
||||
prev.map((status, idx) => (idx === i ? { ...status, checking: false, isValid: valid } : status))
|
||||
@ -65,7 +76,17 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
|
||||
setKeyStatuses((prev) => prev.map((status, idx) => (idx === keyIndex ? { ...status, checking: true } : status)))
|
||||
|
||||
try {
|
||||
const { valid } = await checkApi({ ...provider, apiKey: keyStatuses[keyIndex].key }, model)
|
||||
let valid = false
|
||||
if (type === 'provider' && model) {
|
||||
const result = await checkApi({ ...(provider as Provider), apiKey: keyStatuses[keyIndex].key }, model)
|
||||
valid = result.valid
|
||||
} else {
|
||||
const result = await WebSearchService.checkSearch({
|
||||
...(provider as WebSearchProvider),
|
||||
apiKey: keyStatuses[keyIndex].key
|
||||
})
|
||||
valid = result.valid
|
||||
}
|
||||
|
||||
setKeyStatuses((prev) =>
|
||||
prev.map((status, idx) => (idx === keyIndex ? { ...status, checking: false, isValid: valid } : status))
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { isOpenAIProvider } from '@renderer/providers/ProviderFactory'
|
||||
import { checkApi } from '@renderer/services/ApiService'
|
||||
import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
|
||||
import { checkModelsHealth, ModelCheckStatus } from '@renderer/services/HealthCheckService'
|
||||
import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/services/ProviderService'
|
||||
import { Provider } from '@renderer/types'
|
||||
@ -198,7 +198,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
title: t('settings.provider.check_multiple_keys'),
|
||||
provider: { ...provider, apiHost },
|
||||
model,
|
||||
apiKeys: keys
|
||||
apiKeys: keys,
|
||||
type: 'provider'
|
||||
})
|
||||
|
||||
if (result?.validKeys) {
|
||||
@ -240,10 +241,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
return formatApiHost(apiHost) + 'chat/completions'
|
||||
}
|
||||
|
||||
const formatApiKeys = (value: string) => {
|
||||
return value.replaceAll(',', ',').replaceAll(' ', ',').replaceAll(' ', '').replaceAll('\n', ',')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setApiKey(provider.apiKey)
|
||||
setApiHost(provider.apiHost)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { CheckOutlined, ExportOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { getWebSearchProviderLogo, WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
|
||||
import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders'
|
||||
import { formatApiKeys } from '@renderer/services/ApiService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { WebSearchProvider } from '@renderer/types'
|
||||
import { hasObjectKey } from '@renderer/utils'
|
||||
@ -10,7 +11,8 @@ import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingHelpLink, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
|
||||
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
|
||||
import ApiCheckPopup from '../ProviderSettings/ApiCheckPopup'
|
||||
|
||||
interface Props {
|
||||
provider: WebSearchProvider
|
||||
@ -19,8 +21,8 @@ interface Props {
|
||||
const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const { provider, updateProvider } = useWebSearchProvider(_provider.id)
|
||||
const { t } = useTranslation()
|
||||
const [apiKey, setApiKey] = useState(provider.apiKey)
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [apiKey, setApiKey] = useState(provider.apiKey || '')
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost || '')
|
||||
const [apiChecking, setApiChecking] = useState(false)
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
|
||||
@ -57,27 +59,47 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (apiKey.includes(',')) {
|
||||
const keys = apiKey
|
||||
.split(',')
|
||||
.map((k) => k.trim())
|
||||
.filter((k) => k)
|
||||
|
||||
const result = await ApiCheckPopup.show({
|
||||
title: t('settings.provider.check_multiple_keys'),
|
||||
provider: { ...provider, apiHost },
|
||||
apiKeys: keys,
|
||||
type: 'websearch'
|
||||
})
|
||||
|
||||
if (result?.validKeys) {
|
||||
setApiKey(result.validKeys.join(','))
|
||||
updateProvider({ ...provider, apiKey: result.validKeys.join(',') })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setApiChecking(true)
|
||||
const { valid, error } = await WebSearchService.checkSearch(provider)
|
||||
|
||||
setApiValid(valid)
|
||||
|
||||
if (!valid && error) {
|
||||
const errorMessage = error.message ? ' ' + error.message : ''
|
||||
window.message.error({
|
||||
content: errorMessage,
|
||||
duration: 4,
|
||||
key: 'search-check-error'
|
||||
const errorMessage = error && error?.message ? ' ' + error?.message : ''
|
||||
window.message[valid ? 'success' : 'error']({
|
||||
key: 'api-check',
|
||||
style: { marginTop: '3vh' },
|
||||
duration: valid ? 2 : 8,
|
||||
content: valid ? t('settings.websearch.check_success') : t('settings.websearch.check_failed') + errorMessage
|
||||
})
|
||||
}
|
||||
|
||||
setApiValid(valid)
|
||||
} catch (err) {
|
||||
console.error('Check search error:', err)
|
||||
setApiValid(false)
|
||||
window.message.error({
|
||||
content: t('settings.websearch.check_failed'),
|
||||
duration: 3,
|
||||
key: 'check-search-error'
|
||||
key: 'check-search-error',
|
||||
style: { marginTop: '3vh' },
|
||||
duration: 8,
|
||||
content: t('settings.websearch.check_failed')
|
||||
})
|
||||
} finally {
|
||||
setApiChecking(false)
|
||||
@ -112,7 +134,7 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
<Input.Password
|
||||
value={apiKey}
|
||||
placeholder={t('settings.provider.api_key')}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
onChange={(e) => setApiKey(formatApiKeys(e.target.value))}
|
||||
onBlur={onUpdateApiKey}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
@ -130,6 +152,7 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.websearch.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -308,3 +308,12 @@ export async function fetchModels(provider: Provider) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format API keys
|
||||
* @param value Raw key string
|
||||
* @returns Formatted key string
|
||||
*/
|
||||
export const formatApiKeys = (value: string) => {
|
||||
return value.replaceAll(',', ',').replaceAll(' ', ',').replaceAll(' ', '').replaceAll('\n', ',')
|
||||
}
|
||||
|
||||
@ -3,8 +3,34 @@ import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||
export default abstract class BaseWebSearchProvider {
|
||||
// @ts-ignore this
|
||||
private provider: WebSearchProvider
|
||||
protected apiKey: string
|
||||
|
||||
constructor(provider: WebSearchProvider) {
|
||||
this.provider = provider
|
||||
this.apiKey = this.getApiKey()
|
||||
}
|
||||
|
||||
abstract search(query: string, maxResult: number, excludeDomains: string[]): Promise<WebSearchResponse>
|
||||
|
||||
public getApiKey() {
|
||||
const keys = this.provider.apiKey?.split(',').map((key) => key.trim()) || []
|
||||
const keyName = `web-search-provider:${this.provider.id}:last_used_key`
|
||||
|
||||
if (keys.length === 1) {
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
const lastUsedKey = window.keyv.get(keyName)
|
||||
if (!lastUsedKey) {
|
||||
window.keyv.set(keyName, keys[0])
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
const currentIndex = keys.indexOf(lastUsedKey)
|
||||
const nextIndex = (currentIndex + 1) % keys.length
|
||||
const nextKey = keys[nextIndex]
|
||||
window.keyv.set(keyName, nextKey)
|
||||
|
||||
return nextKey
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user