mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-10-25 17:36:29 +08:00
feat: support tavily and searxng in desktop search settings
Extend the desktop search credential UI to cover the two new providers, matching the web console: Tavily uses the shared API-key modal, and SearXNG gets an instance-URL field (prefilled from url_masked, no masking). Adds the needs_url/url_masked types and the tavily/searxng i18n strings. Co-authored-by: cowagent <cow@cowagent.ai>
This commit is contained in:
1 parent
24c4105d93
commit
ffb789bbb7
3 files changed
+71
-27
No files matched your search
@@ -479,6 +479,11 @@ const translations: Record<string, Record<string, string>> = {
|
||||
models_search_anysearch_anon_hint: '留空可启用匿名模式(无需 API Key,每日有免费额度)',
|
||||
models_search_serply_title: '配置 Serply API Key',
|
||||
models_search_serply_desc: '前往 serply.io 控制台创建 API Key。',
|
||||
models_search_tavily_title: '配置 Tavily API Key',
|
||||
models_search_tavily_desc: '前往 tavily.com 控制台创建 API Key。',
|
||||
models_search_searxng_title: '配置 SearXNG 实例 URL',
|
||||
models_search_searxng_desc: '输入自托管 SearXNG 实例的 URL(无需 API Key)。',
|
||||
models_search_instance_url: '实例 URL',
|
||||
skills_title: '技能管理',
|
||||
skills_desc: '查看、启用或禁用 Agent 工具和技能',
|
||||
skills_hub_btn: '探索技能广场',
|
||||
@@ -1192,6 +1197,11 @@ const translations: Record<string, Record<string, string>> = {
|
||||
models_search_anysearch_anon_hint: 'Leave empty to enable anonymous mode (no API key required, daily free quota)',
|
||||
models_search_serply_title: 'Configure Serply API Key',
|
||||
models_search_serply_desc: 'Create a key at the Serply console (serply.io).',
|
||||
models_search_tavily_title: 'Configure Tavily API Key',
|
||||
models_search_tavily_desc: 'Create a key at the Tavily console (tavily.com).',
|
||||
models_search_searxng_title: 'Configure SearXNG Instance URL',
|
||||
models_search_searxng_desc: 'Enter the URL of your self-hosted SearXNG instance (no API key required).',
|
||||
models_search_instance_url: 'Instance URL',
|
||||
config_max_tokens: 'Max Context Tokens',
|
||||
config_max_turns: 'Max Memory Turns',
|
||||
config_max_steps: 'Max Steps',
|
||||
|
||||
@@ -236,8 +236,14 @@ const ModelsTab: React.FC<ModelsTabProps> = ({ baseUrl }) => {
|
||||
onSaveStrategy={(strategy, provider) =>
|
||||
run('search', { action: 'set_capability', capability: 'search', strategy, provider })
|
||||
}
|
||||
onSaveSearchKey={(provider, key, anonymous) =>
|
||||
run('search_key', { action: 'set_search_credential', provider, api_key: key, anonymous })
|
||||
onSaveSearchKey={(provider, value, anonymous) =>
|
||||
run(
|
||||
'search_key',
|
||||
provider === 'searxng'
|
||||
? // SearXNG persists an instance URL, not an API key.
|
||||
{ action: 'set_search_credential', provider, url: value }
|
||||
: { action: 'set_search_credential', provider, api_key: value, anonymous }
|
||||
)
|
||||
}
|
||||
keyStatus={statusMap.search_key}
|
||||
keyBusy={busy === 'search_key'}
|
||||
@@ -793,10 +799,11 @@ const EmbeddingCard: React.FC<{
|
||||
)
|
||||
|
||||
// Search providers that own a dedicated credential (as opposed to reusing a
|
||||
// model-vendor key). AnySearch additionally supports an anonymous tier, so it
|
||||
// model-vendor key): a dedicated API key, or — for SearXNG — a self-hosted
|
||||
// instance URL. AnySearch additionally supports an anonymous tier, so it
|
||||
// counts as "configured" even without a key. Mirrors the web console flow.
|
||||
const isDedicatedKeyProvider = (p: SearchProviderMeta): boolean =>
|
||||
p.needs_dedicated_key || ['bocha', 'anysearch', 'serply'].includes(p.id)
|
||||
p.needs_dedicated_key || p.needs_url || ['bocha', 'anysearch', 'serply', 'tavily', 'searxng'].includes(p.id)
|
||||
|
||||
const SearchCard: React.FC<{
|
||||
state: SearchCapabilityState
|
||||
@@ -945,27 +952,31 @@ const SearchCard: React.FC<{
|
||||
)
|
||||
}
|
||||
|
||||
// Dedicated-key editor for a single search provider. AnySearch adds an
|
||||
// anonymous option: saving with an empty key enables the anonymous tier.
|
||||
// Dedicated-credential editor for a single search provider. Most providers
|
||||
// hold an API key; SearXNG holds a self-hosted instance URL (echoed back
|
||||
// verbatim, not masked). AnySearch adds an anonymous option: saving with an
|
||||
// empty key enables the anonymous tier.
|
||||
const SearchKeyModal: React.FC<{
|
||||
provider: SearchProviderMeta | null
|
||||
busy: boolean
|
||||
status?: string
|
||||
onClose: () => void
|
||||
onSave: (key: string, anonymous: boolean) => void
|
||||
onSave: (value: string, anonymous: boolean) => void
|
||||
}> = ({ provider, busy, onClose, onSave }) => {
|
||||
const open = !!provider
|
||||
const masked = provider?.api_key_masked || ''
|
||||
const isAnysearch = provider?.id === 'anysearch'
|
||||
const [key, setKey] = useState('')
|
||||
const isSearxng = provider?.id === 'searxng' || !!provider?.needs_url
|
||||
// SearXNG prefills its plain instance URL; others prefill the masked key.
|
||||
const initial = isSearxng ? provider?.url_masked || '' : provider?.api_key_masked || ''
|
||||
const [value, setValue] = useState('')
|
||||
const [dirty, setDirty] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setKey(masked)
|
||||
setValue(initial)
|
||||
setDirty(false)
|
||||
}
|
||||
}, [open, masked])
|
||||
}, [open, initial])
|
||||
|
||||
if (!provider) return null
|
||||
|
||||
@@ -973,18 +984,37 @@ const SearchKeyModal: React.FC<{
|
||||
const desc = t(`models_search_${provider.id}_desc`)
|
||||
|
||||
const handleSave = () => {
|
||||
if (isSearxng) {
|
||||
// URL is plain text (never masked). Empty is a no-op here — use the
|
||||
// configured chip / clear flow to remove it.
|
||||
const trimmed = value.trim()
|
||||
if (!dirty || !trimmed) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
onSave(trimmed, false)
|
||||
return
|
||||
}
|
||||
// Kept the masked placeholder untouched -> nothing to persist.
|
||||
if (!dirty || MASK_RE.test(key)) {
|
||||
if (!dirty || MASK_RE.test(value)) {
|
||||
// AnySearch: an untouched-but-empty field still means "anonymous".
|
||||
if (isAnysearch && !masked) onSave('', true)
|
||||
if (isAnysearch && !initial) onSave('', true)
|
||||
else onClose()
|
||||
return
|
||||
}
|
||||
const trimmed = key.trim()
|
||||
const trimmed = value.trim()
|
||||
// AnySearch: empty key = enable anonymous mode.
|
||||
onSave(trimmed, isAnysearch && !trimmed)
|
||||
}
|
||||
|
||||
const fieldLabel = isSearxng ? t('models_search_instance_url') : 'API Key'
|
||||
const fieldHint = isSearxng
|
||||
? undefined
|
||||
: isAnysearch
|
||||
? t('models_search_anysearch_anon_hint')
|
||||
: undefined
|
||||
const placeholder = isSearxng ? 'https://searxng.example.com' : 'sk-...'
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -1002,19 +1032,17 @@ const SearchKeyModal: React.FC<{
|
||||
}
|
||||
>
|
||||
<p className="text-xs text-content-tertiary mb-3">{desc}</p>
|
||||
<Field
|
||||
label="API Key"
|
||||
hint={isAnysearch ? t('models_search_anysearch_anon_hint') : undefined}
|
||||
>
|
||||
<Field label={fieldLabel} hint={fieldHint}>
|
||||
<TextInput
|
||||
className="font-mono"
|
||||
value={key}
|
||||
placeholder="sk-..."
|
||||
className={isSearxng ? '' : 'font-mono'}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onFocus={() => {
|
||||
if (!dirty && MASK_RE.test(key)) setKey('')
|
||||
// Only API keys use a masked sentinel; URLs stay as-is.
|
||||
if (!isSearxng && !dirty && MASK_RE.test(value)) setValue('')
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setKey(e.target.value)
|
||||
setValue(e.target.value)
|
||||
setDirty(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -600,6 +600,11 @@ export interface SearchProviderMeta {
|
||||
// AnySearch can be "configured" via anonymous mode (no key). The backend
|
||||
// sets this flag so the UI can badge it and offer the anonymous opt-in.
|
||||
anonymous?: boolean
|
||||
// SearXNG holds a self-hosted instance URL instead of an API key. When
|
||||
// needs_url is set the editor shows a URL field; url_masked echoes the saved
|
||||
// URL (not a secret, so returned verbatim) for prefill/edit.
|
||||
needs_url?: boolean
|
||||
url_masked?: string
|
||||
}
|
||||
|
||||
export interface CapabilityState {
|
||||
@@ -685,10 +690,11 @@ export type ModelsAction =
|
||||
// is accepted here alongside its opt-in fields.
|
||||
| { action: 'set_capability'; capability: CapabilityKey | 'chat_fallback'; provider_id?: string; model?: string; voice?: string; strategy?: string; provider?: string; enabled?: boolean; max_switches?: number }
|
||||
| { action: 'set_voice_reply_mode'; mode: 'off' | 'voice_if_voice' | 'always' }
|
||||
// Dedicated search-provider credentials (bocha / anysearch / serply). The
|
||||
// provider field defaults to bocha server-side when omitted; anonymous is
|
||||
// AnySearch-only (save with an empty key to enable the anonymous tier).
|
||||
| { action: 'set_search_credential'; provider?: string; api_key: string; anonymous?: boolean }
|
||||
// Dedicated search-provider credentials (bocha / anysearch / serply / tavily
|
||||
// use api_key; searxng uses url). The provider field defaults to bocha
|
||||
// server-side when omitted; anonymous is AnySearch-only (save with an empty
|
||||
// key to enable the anonymous tier).
|
||||
| { action: 'set_search_credential'; provider?: string; api_key?: string; url?: string; anonymous?: boolean }
|
||||
|
||||
// ============================================================
|
||||
// Channels
|
||||
|
||||
Reference in new issue
Block a user