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": "API Key",
|
||||||
"api_key.tip": "Multiple keys separated by commas",
|
"api_key.tip": "Multiple keys separated by commas",
|
||||||
"api_version": "API Version",
|
"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",
|
"charge": "Charge",
|
||||||
"check": "Check",
|
"check": "Check",
|
||||||
"check_all_keys": "Check All Keys",
|
"check_all_keys": "Check All Keys",
|
||||||
|
|||||||
@ -1229,6 +1229,12 @@
|
|||||||
"api_key": "APIキー",
|
"api_key": "APIキー",
|
||||||
"api_key.tip": "複数のキーはカンマで区切ります",
|
"api_key.tip": "複数のキーはカンマで区切ります",
|
||||||
"api_version": "APIバージョン",
|
"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": "充電",
|
"charge": "充電",
|
||||||
"check": "チェック",
|
"check": "チェック",
|
||||||
"check_all_keys": "すべてのキーをチェック",
|
"check_all_keys": "すべてのキーをチェック",
|
||||||
|
|||||||
@ -1229,6 +1229,12 @@
|
|||||||
"api_key": "Ключ API",
|
"api_key": "Ключ API",
|
||||||
"api_key.tip": "Несколько ключей, разделенных запятыми",
|
"api_key.tip": "Несколько ключей, разделенных запятыми",
|
||||||
"api_version": "Версия API",
|
"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": "Пополнить",
|
"charge": "Пополнить",
|
||||||
"check": "Проверить",
|
"check": "Проверить",
|
||||||
"check_all_keys": "Проверить все ключи",
|
"check_all_keys": "Проверить все ключи",
|
||||||
|
|||||||
@ -1230,6 +1230,12 @@
|
|||||||
"api_key": "API 密钥",
|
"api_key": "API 密钥",
|
||||||
"api_key.tip": "多个密钥使用逗号分隔",
|
"api_key.tip": "多个密钥使用逗号分隔",
|
||||||
"api_version": "API 版本",
|
"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": "充值",
|
"charge": "充值",
|
||||||
"check": "检查",
|
"check": "检查",
|
||||||
"check_all_keys": "检查所有密钥",
|
"check_all_keys": "检查所有密钥",
|
||||||
|
|||||||
@ -1229,6 +1229,12 @@
|
|||||||
"api_key": "API 金鑰",
|
"api_key": "API 金鑰",
|
||||||
"api_key.tip": "多個金鑰使用逗號分隔",
|
"api_key.tip": "多個金鑰使用逗號分隔",
|
||||||
"api_version": "API 版本",
|
"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": "儲值",
|
"charge": "儲值",
|
||||||
"check": "檢查",
|
"check": "檢查",
|
||||||
"check_all_keys": "檢查所有金鑰",
|
"check_all_keys": "檢查所有金鑰",
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import { formatApiKeys } from '@renderer/services/ApiService'
|
|||||||
import WebSearchService from '@renderer/services/WebSearchService'
|
import WebSearchService from '@renderer/services/WebSearchService'
|
||||||
import { WebSearchProvider } from '@renderer/types'
|
import { WebSearchProvider } from '@renderer/types'
|
||||||
import { hasObjectKey } from '@renderer/utils'
|
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 Link from 'antd/es/typography/Link'
|
||||||
import { Info } from 'lucide-react'
|
import { Info } from 'lucide-react'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
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'
|
import ApiCheckPopup from '../ProviderSettings/ApiCheckPopup'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -25,6 +25,8 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
const [apiKey, setApiKey] = useState(provider.apiKey || '')
|
const [apiKey, setApiKey] = useState(provider.apiKey || '')
|
||||||
const [apiHost, setApiHost] = useState(provider.apiHost || '')
|
const [apiHost, setApiHost] = useState(provider.apiHost || '')
|
||||||
const [apiChecking, setApiChecking] = useState(false)
|
const [apiChecking, setApiChecking] = useState(false)
|
||||||
|
const [basicAuthUsername, setBasicAuthUsername] = useState(provider.basicAuthUsername || '')
|
||||||
|
const [basicAuthPassword, setBasicAuthPassword] = useState(provider.basicAuthPassword || '')
|
||||||
const [apiValid, setApiValid] = useState(false)
|
const [apiValid, setApiValid] = useState(false)
|
||||||
|
|
||||||
const webSearchProviderConfig = WEB_SEARCH_PROVIDER_CONFIG[provider.id]
|
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() {
|
async function checkSearch() {
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
window.message.error({
|
window.message.error({
|
||||||
@ -111,7 +133,9 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setApiKey(provider.apiKey ?? '')
|
setApiKey(provider.apiKey ?? '')
|
||||||
setApiHost(provider.apiHost ?? '')
|
setApiHost(provider.apiHost ?? '')
|
||||||
}, [provider.apiKey, provider.apiHost])
|
setBasicAuthUsername(provider.basicAuthUsername ?? '')
|
||||||
|
setBasicAuthPassword(provider.basicAuthPassword ?? '')
|
||||||
|
}, [provider.apiKey, provider.apiHost, provider.basicAuthUsername, provider.basicAuthPassword])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -176,6 +200,50 @@ const WebSearchProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.websearch.check')}
|
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.websearch.check')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</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 { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import ky from 'ky'
|
||||||
|
|
||||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||||
|
|
||||||
@ -9,6 +10,8 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
|||||||
private searxng: SearxngClient
|
private searxng: SearxngClient
|
||||||
private engines: string[] = []
|
private engines: string[] = []
|
||||||
private readonly apiHost: string
|
private readonly apiHost: string
|
||||||
|
private readonly basicAuthUsername?: string
|
||||||
|
private readonly basicAuthPassword?: string
|
||||||
private isInitialized = false
|
private isInitialized = false
|
||||||
|
|
||||||
constructor(provider: WebSearchProvider) {
|
constructor(provider: WebSearchProvider) {
|
||||||
@ -16,9 +19,22 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
|||||||
if (!provider.apiHost) {
|
if (!provider.apiHost) {
|
||||||
throw new Error('API host is required for SearxNG provider')
|
throw new Error('API host is required for SearxNG provider')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.apiHost = provider.apiHost
|
this.apiHost = provider.apiHost
|
||||||
|
this.basicAuthUsername = provider.basicAuthUsername
|
||||||
|
this.basicAuthPassword = provider.basicAuthPassword ? provider.basicAuthPassword : ''
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to initialize SearxNG client: ${error instanceof Error ? error.message : 'Unknown 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> {
|
private async initEngines(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`Initializing SearxNG with API host: ${this.apiHost}`)
|
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`, {
|
const response = await axios.get(`${this.apiHost}/config`, {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
validateStatus: (status) => status === 200 // 仅接受 200 状态码
|
validateStatus: (status) => status === 200, // 仅接受 200 状态码
|
||||||
|
auth
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
|
|||||||
@ -1236,6 +1236,19 @@ const migrateConfig = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return state
|
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
|
apiHost?: string
|
||||||
engines?: string[]
|
engines?: string[]
|
||||||
url?: string
|
url?: string
|
||||||
|
basicAuthUsername?: string
|
||||||
|
basicAuthPassword?: string
|
||||||
contentLimit?: number
|
contentLimit?: number
|
||||||
usingBrowser?: boolean
|
usingBrowser?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user