feat(websearch): HTTP basic auth support for Searxng (#5009)
* feat(websearch): HTTP basic auth support for Searxng * fix(websearch): schema migration * refactor(i18n): update translations for basic authentication * fix(websearch): 修正 `HTTP 认证` 相关翻译 * feat(websearch): 为 `HTTP 认证` 添加注释 --------- Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
parent
3360905275
commit
1c5adc1329
@ -1230,6 +1230,12 @@
|
||||
"api_key": "API Key",
|
||||
"api_key.tip": "Multiple keys separated by commas",
|
||||
"api_version": "API Version",
|
||||
"basic_auth": "HTTP authentication",
|
||||
"basic_auth.tip": "Applicable to instances deployed remotely (see the documentation). Currently, only the Basic scheme (RFC 7617) is supported.",
|
||||
"basic_auth.user_name": "Username",
|
||||
"basic_auth.user_name.tip": "Left empty to disable",
|
||||
"basic_auth.password": "Password",
|
||||
"basic_auth.password.tip": "",
|
||||
"charge": "Charge",
|
||||
"check": "Check",
|
||||
"check_all_keys": "Check All Keys",
|
||||
|
||||
@ -1229,6 +1229,12 @@
|
||||
"api_key": "APIキー",
|
||||
"api_key.tip": "複数のキーはカンマで区切ります",
|
||||
"api_version": "APIバージョン",
|
||||
"basic_auth": "HTTP 認証",
|
||||
"basic_auth.tip": "サーバー展開によるインスタンスに適用されます(ドキュメントを参照)。現在はBasicスキーム(RFC7617)のみをサポートしています。",
|
||||
"basic_auth.user_name": "ユーザー名",
|
||||
"basic_auth.user_name.tip": "空欄で無効化",
|
||||
"basic_auth.password": "パスワード",
|
||||
"basic_auth.password.tip": "",
|
||||
"charge": "充電",
|
||||
"check": "チェック",
|
||||
"check_all_keys": "すべてのキーをチェック",
|
||||
|
||||
@ -1229,6 +1229,12 @@
|
||||
"api_key": "Ключ API",
|
||||
"api_key.tip": "Несколько ключей, разделенных запятыми",
|
||||
"api_version": "Версия API",
|
||||
"basic_auth": "HTTP аутентификация",
|
||||
"basic_auth.tip": "Применимо к экземплярам, развернутым через сервер (см. документацию). В настоящее время поддерживается только схема Basic (RFC7617).",
|
||||
"basic_auth.user_name": "Имя пользователя",
|
||||
"basic_auth.user_name.tip": "Оставить пустым для отключения",
|
||||
"basic_auth.password": "Пароль",
|
||||
"basic_auth.password.tip": "",
|
||||
"charge": "Пополнить",
|
||||
"check": "Проверить",
|
||||
"check_all_keys": "Проверить все ключи",
|
||||
|
||||
@ -1230,6 +1230,12 @@
|
||||
"api_key": "API 密钥",
|
||||
"api_key.tip": "多个密钥使用逗号分隔",
|
||||
"api_version": "API 版本",
|
||||
"basic_auth": "HTTP 认证",
|
||||
"basic_auth.tip": "适用于通过服务器部署的实例(参见文档)。目前仅支持 Basic 方案(RFC7617)。",
|
||||
"basic_auth.user_name": "用户名",
|
||||
"basic_auth.user_name.tip": "留空以禁用",
|
||||
"basic_auth.password": "密码",
|
||||
"basic_auth.password.tip": "",
|
||||
"charge": "充值",
|
||||
"check": "检查",
|
||||
"check_all_keys": "检查所有密钥",
|
||||
|
||||
@ -1229,6 +1229,12 @@
|
||||
"api_key": "API 金鑰",
|
||||
"api_key.tip": "多個金鑰使用逗號分隔",
|
||||
"api_version": "API 版本",
|
||||
"basic_auth": "HTTP 認證",
|
||||
"basic_auth.tip": "適用於透過伺服器部署的實例(請參閱文檔)。目前僅支援 Basic 方案(RFC7617)。",
|
||||
"basic_auth.user_name": "用戶",
|
||||
"basic_auth.user_name.tip": "留空以停用",
|
||||
"basic_auth.password": "密碼",
|
||||
"basic_auth.password.tip": "",
|
||||
"charge": "儲值",
|
||||
"check": "檢查",
|
||||
"check_all_keys": "檢查所有金鑰",
|
||||
|
||||
@ -5,14 +5,14 @@ import { formatApiKeys } from '@renderer/services/ApiService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { WebSearchProvider } from '@renderer/types'
|
||||
import { hasObjectKey } from '@renderer/utils'
|
||||
import { Avatar, Button, Divider, Flex, Input } from 'antd'
|
||||
import { Avatar, Button, Divider, Flex, Form, Input, Tooltip } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { Info } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
|
||||
import { SettingDivider, SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
|
||||
import ApiCheckPopup from '../ProviderSettings/ApiCheckPopup'
|
||||
|
||||
interface Props {
|
||||
@ -25,6 +25,8 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const [apiKey, setApiKey] = useState(provider.apiKey || '')
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost || '')
|
||||
const [apiChecking, setApiChecking] = useState(false)
|
||||
const [basicAuthUsername, setBasicAuthUsername] = useState(provider.basicAuthUsername || '')
|
||||
const [basicAuthPassword, setBasicAuthPassword] = useState(provider.basicAuthPassword || '')
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
|
||||
const webSearchProviderConfig = WEB_SEARCH_PROVIDER_CONFIG[provider.id]
|
||||
@ -49,6 +51,26 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateBasicAuthUsername = () => {
|
||||
const currentValue = basicAuthUsername || ''
|
||||
const savedValue = provider.basicAuthUsername || ''
|
||||
if (currentValue !== savedValue) {
|
||||
updateProvider({ ...provider, basicAuthUsername: basicAuthUsername })
|
||||
} else {
|
||||
setBasicAuthUsername(provider.basicAuthUsername || '')
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateBasicAuthPassword = () => {
|
||||
const currentValue = basicAuthPassword || ''
|
||||
const savedValue = provider.basicAuthPassword || ''
|
||||
if (currentValue !== savedValue) {
|
||||
updateProvider({ ...provider, basicAuthPassword: basicAuthPassword })
|
||||
} else {
|
||||
setBasicAuthPassword(provider.basicAuthPassword || '')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSearch() {
|
||||
if (!provider) {
|
||||
window.message.error({
|
||||
@ -111,7 +133,9 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
useEffect(() => {
|
||||
setApiKey(provider.apiKey ?? '')
|
||||
setApiHost(provider.apiHost ?? '')
|
||||
}, [provider.apiKey, provider.apiHost])
|
||||
setBasicAuthUsername(provider.basicAuthUsername ?? '')
|
||||
setBasicAuthPassword(provider.basicAuthPassword ?? '')
|
||||
}, [provider.apiKey, provider.apiHost, provider.basicAuthUsername, provider.basicAuthPassword])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -176,6 +200,50 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.websearch.check')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<SettingDivider style={{ marginTop: 12, marginBottom: 12 }} />
|
||||
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
{t('settings.provider.basic_auth')}
|
||||
<Tooltip title={t('settings.provider.basic_auth.tip')} placement="right">
|
||||
<Info size={16} color="var(--color-icon)" style={{ marginLeft: 5, cursor: 'pointer' }} />
|
||||
</Tooltip>
|
||||
</SettingSubtitle>
|
||||
<Flex>
|
||||
<Form
|
||||
layout="inline"
|
||||
initialValues={{
|
||||
username: basicAuthUsername,
|
||||
password: basicAuthPassword
|
||||
}}
|
||||
onValuesChange={(changedValues) => {
|
||||
// Update local state when form values change
|
||||
if ('username' in changedValues) {
|
||||
setBasicAuthUsername(changedValues.username || '')
|
||||
}
|
||||
if ('password' in changedValues) {
|
||||
setBasicAuthPassword(changedValues.password || '')
|
||||
}
|
||||
}}>
|
||||
<Form.Item label={t('settings.provider.basic_auth.user_name')} name="username">
|
||||
<Input
|
||||
placeholder={t('settings.provider.basic_auth.user_name.tip')}
|
||||
onBlur={onUpdateBasicAuthUsername}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('settings.provider.basic_auth.password')}
|
||||
name="password"
|
||||
rules={[{ required: !!basicAuthUsername, validateTrigger: ['onBlur', 'onChange'] }]}
|
||||
help=""
|
||||
hidden={!basicAuthUsername}>
|
||||
<Input.Password
|
||||
placeholder={t('settings.provider.basic_auth.password.tip')}
|
||||
onBlur={onUpdateBasicAuthPassword}
|
||||
disabled={!basicAuthUsername}
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -2,6 +2,7 @@ import { SearxngClient } from '@agentic/searxng'
|
||||
import { WebSearchState } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||
import axios from 'axios'
|
||||
import ky from 'ky'
|
||||
|
||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||
|
||||
@ -9,6 +10,8 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
||||
private searxng: SearxngClient
|
||||
private engines: string[] = []
|
||||
private readonly apiHost: string
|
||||
private readonly basicAuthUsername?: string
|
||||
private readonly basicAuthPassword?: string
|
||||
private isInitialized = false
|
||||
|
||||
constructor(provider: WebSearchProvider) {
|
||||
@ -16,9 +19,22 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
||||
if (!provider.apiHost) {
|
||||
throw new Error('API host is required for SearxNG provider')
|
||||
}
|
||||
|
||||
this.apiHost = provider.apiHost
|
||||
this.basicAuthUsername = provider.basicAuthUsername
|
||||
this.basicAuthPassword = provider.basicAuthPassword ? provider.basicAuthPassword : ''
|
||||
|
||||
try {
|
||||
this.searxng = new SearxngClient({ apiBaseUrl: this.apiHost })
|
||||
// `ky` do not support basic auth directly
|
||||
const headers = this.basicAuthUsername
|
||||
? {
|
||||
Authorization: `Basic ` + btoa(`${this.basicAuthUsername}:${this.basicAuthPassword}`)
|
||||
}
|
||||
: undefined
|
||||
this.searxng = new SearxngClient({
|
||||
apiBaseUrl: this.apiHost,
|
||||
ky: ky.create({ headers })
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to initialize SearxNG client: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
@ -29,9 +45,16 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
||||
private async initEngines(): Promise<void> {
|
||||
try {
|
||||
console.log(`Initializing SearxNG with API host: ${this.apiHost}`)
|
||||
const auth = this.basicAuthUsername
|
||||
? {
|
||||
username: this.basicAuthUsername,
|
||||
password: this.basicAuthPassword ? this.basicAuthPassword : ''
|
||||
}
|
||||
: undefined
|
||||
const response = await axios.get(`${this.apiHost}/config`, {
|
||||
timeout: 5000,
|
||||
validateStatus: (status) => status === 200 // 仅接受 200 状态码
|
||||
validateStatus: (status) => status === 200, // 仅接受 200 状态码
|
||||
auth
|
||||
})
|
||||
|
||||
if (!response.data) {
|
||||
|
||||
@ -1236,6 +1236,19 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'98': (state: RootState) => {
|
||||
try {
|
||||
if (state.websearch && state.websearch.providers) {
|
||||
state.websearch.providers.forEach((provider) => {
|
||||
provider.basicAuthUsername = ''
|
||||
provider.basicAuthPassword = ''
|
||||
})
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -346,6 +346,8 @@ export type WebSearchProvider = {
|
||||
apiHost?: string
|
||||
engines?: string[]
|
||||
url?: string
|
||||
basicAuthUsername?: string
|
||||
basicAuthPassword?: string
|
||||
contentLimit?: number
|
||||
usingBrowser?: boolean
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user