From 1c5adc1329ac77d41c5d0728be3f4f1d2099064e Mon Sep 17 00:00:00 2001 From: Hantong Chen <70561268+cxw620@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:58:03 +0800 Subject: [PATCH] feat(websearch): HTTP basic auth support for Searxng (#5009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/renderer/src/i18n/locales/en-us.json | 6 ++ src/renderer/src/i18n/locales/ja-jp.json | 6 ++ src/renderer/src/i18n/locales/ru-ru.json | 6 ++ src/renderer/src/i18n/locales/zh-cn.json | 6 ++ src/renderer/src/i18n/locales/zh-tw.json | 6 ++ .../WebSearchProviderSetting.tsx | 74 ++++++++++++++++++- .../WebSearchProvider/SearxngProvider.ts | 27 ++++++- src/renderer/src/store/migrate.ts | 13 ++++ src/renderer/src/types/index.ts | 2 + 9 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index d3fcdc80..2ff1ce6e 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index f1eeab44..21c7db52 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": "すべてのキーをチェック", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 653494fb..ad26a716 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": "Проверить все ключи", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index cf089664..975bf2b1 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "检查所有密钥", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e5bf357f..5ec1ee63 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "檢查所有金鑰", diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx index 25ba334f..f19f192e 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ provider: _provider }) => { {apiChecking ? : apiValid ? : t('settings.websearch.check')} + + + {t('settings.provider.basic_auth')} + + + + + +
{ + // Update local state when form values change + if ('username' in changedValues) { + setBasicAuthUsername(changedValues.username || '') + } + if ('password' in changedValues) { + setBasicAuthPassword(changedValues.password || '') + } + }}> + + + + +
+
)} diff --git a/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts b/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts index ad877ba1..0e95c12c 100644 --- a/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts @@ -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 { 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) { diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index b0280e00..2a1318b4 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -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 + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 83f34280..dfefc2ec 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -346,6 +346,8 @@ export type WebSearchProvider = { apiHost?: string engines?: string[] url?: string + basicAuthUsername?: string + basicAuthPassword?: string contentLimit?: number usingBrowser?: boolean }