From c47843fbdf11ee13e9a99a2d11a4d10465686711 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Thu, 21 May 2026 23:33:45 +0800 Subject: [PATCH] feat: add Tavily, Bing, Google, SerpAPI, and Exa web search backends Expand web_search tool from 4 backends to 9, covering the most popular search APIs. Priority: Serper > Tavily > Bing > Google > SerpAPI > Brave > Exa > Bocha > DuckDuckGo (free fallback). Co-authored-by: Cursor --- deploy/.env.example | 7 + packages/cli/src/commands/start.ts | 18 ++ packages/core/src/tools/web-search.ts | 182 +++++++++++++++++- packages/org-manager/src/api-server.ts | 40 ++++ packages/shared/src/utils/config.ts | 12 +- packages/web-ui/src/api.ts | 6 +- packages/web-ui/src/components/Onboarding.tsx | 35 +++- .../web-ui/src/locales/en/onboarding.json | 5 + packages/web-ui/src/locales/en/settings.json | 13 +- .../web-ui/src/locales/zh-CN/onboarding.json | 5 + .../web-ui/src/locales/zh-CN/settings.json | 13 +- packages/web-ui/src/pages/Settings.tsx | 32 ++- 12 files changed, 348 insertions(+), 20 deletions(-) diff --git a/deploy/.env.example b/deploy/.env.example index 1ef70b4d..f948bd40 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -20,8 +20,15 @@ COMM_PORT=8058 # FEISHU_WEBHOOK_PORT=9000 # ─── Search API Keys (optional, enables web_search tool) ─────────────────── +# Priority: Serper > Tavily > Bing > Google > SerpAPI > Brave > Exa > Bocha > DuckDuckGo (free) # SERPER_API_KEY= +# TAVILY_API_KEY= +# BING_SEARCH_API_KEY= +# GOOGLE_SEARCH_API_KEY= +# GOOGLE_SEARCH_CX= +# SERPAPI_API_KEY= # BRAVE_SEARCH_API_KEY= +# EXA_API_KEY= # BOCHA_API_KEY= # ─── Security ────────────────────────────────────────────────────────────── diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 96bc82ba..7518df9f 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -443,9 +443,27 @@ async function startServer(config: ReturnType, values: Record if (config.integrations?.search?.serperApiKey && !process.env['SERPER_API_KEY']) { process.env['SERPER_API_KEY'] = config.integrations.search.serperApiKey; } + if (config.integrations?.search?.tavilyApiKey && !process.env['TAVILY_API_KEY']) { + process.env['TAVILY_API_KEY'] = config.integrations.search.tavilyApiKey; + } + if (config.integrations?.search?.bingApiKey && !process.env['BING_SEARCH_API_KEY']) { + process.env['BING_SEARCH_API_KEY'] = config.integrations.search.bingApiKey; + } + if (config.integrations?.search?.googleSearchApiKey && !process.env['GOOGLE_SEARCH_API_KEY']) { + process.env['GOOGLE_SEARCH_API_KEY'] = config.integrations.search.googleSearchApiKey; + } + if (config.integrations?.search?.googleSearchCx && !process.env['GOOGLE_SEARCH_CX']) { + process.env['GOOGLE_SEARCH_CX'] = config.integrations.search.googleSearchCx; + } + if (config.integrations?.search?.serpApiKey && !process.env['SERPAPI_API_KEY']) { + process.env['SERPAPI_API_KEY'] = config.integrations.search.serpApiKey; + } if (config.integrations?.search?.braveApiKey && !process.env['BRAVE_SEARCH_API_KEY']) { process.env['BRAVE_SEARCH_API_KEY'] = config.integrations.search.braveApiKey; } + if (config.integrations?.search?.exaApiKey && !process.env['EXA_API_KEY']) { + process.env['EXA_API_KEY'] = config.integrations.search.exaApiKey; + } if (config.integrations?.search?.bochaApiKey && !process.env['BOCHA_API_KEY']) { process.env['BOCHA_API_KEY'] = config.integrations.search.bochaApiKey; } diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts index 5cc467b6..3cd471b7 100644 --- a/packages/core/src/tools/web-search.ts +++ b/packages/core/src/tools/web-search.ts @@ -51,7 +51,7 @@ async function proxyFetch(url: string | URL, init?: RequestInit): Promise Brave Search > Bocha > DuckDuckGo Lite/HTML fallback. + * Priority: Serper > Tavily > Bing > Google > SerpAPI > Brave > Exa > Bocha > DuckDuckGo (free fallback). * API keys are read from environment variables. */ export const WebSearchTool: AgentToolHandler = { @@ -80,7 +80,12 @@ export const WebSearchTool: AgentToolHandler = { const backends: Array<{ name: string; fn: typeof searchSerper }> = [ { name: 'Serper', fn: searchSerper }, + { name: 'Tavily', fn: searchTavily }, + { name: 'Bing', fn: searchBing }, + { name: 'Google', fn: searchGoogle }, + { name: 'SerpAPI', fn: searchSerpApi }, { name: 'Brave', fn: searchBrave }, + { name: 'Exa', fn: searchExa }, { name: 'Bocha', fn: searchBocha }, { name: 'DuckDuckGo', fn: searchDuckDuckGo }, ]; @@ -152,6 +157,142 @@ async function searchSerper(query: string, maxResults: number): Promise { + const apiKey = process.env['TAVILY_API_KEY']; + if (!apiKey) throw new Error('TAVILY_API_KEY not configured'); + + let res: Response; + try { + res = await proxyFetch('https://api.tavily.com/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + api_key: apiKey, + query, + max_results: maxResults, + include_answer: false, + }), + }); + } catch (err: unknown) { + throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); + } + + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + + const data = (await res.json()) as { + results?: Array<{ title: string; url: string; content: string; published_date?: string }>; + }; + + return (data.results ?? []).slice(0, maxResults).map(r => ({ + title: r.title, + url: r.url, + snippet: r.content, + date: r.published_date, + })); +} + +// ── Bing Web Search backend ────────────────────────────────────────────── + +async function searchBing(query: string, maxResults: number): Promise { + const apiKey = process.env['BING_SEARCH_API_KEY']; + if (!apiKey) throw new Error('BING_SEARCH_API_KEY not configured'); + + const params = new URLSearchParams({ q: query, count: String(maxResults), mkt: 'en-US' }); + let res: Response; + try { + res = await proxyFetch(`https://api.bing.microsoft.com/v7.0/search?${params}`, { + headers: { 'Ocp-Apim-Subscription-Key': apiKey }, + }); + } catch (err: unknown) { + throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); + } + + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + + const data = (await res.json()) as { + webPages?: { + value?: Array<{ name: string; url: string; snippet: string; dateLastCrawled?: string }>; + }; + }; + + return (data.webPages?.value ?? []).slice(0, maxResults).map(r => ({ + title: r.name, + url: r.url, + snippet: r.snippet, + date: r.dateLastCrawled, + })); +} + +// ── Google Custom Search (Programmable Search Engine) backend ───────────── + +async function searchGoogle(query: string, maxResults: number): Promise { + const apiKey = process.env['GOOGLE_SEARCH_API_KEY']; + const cx = process.env['GOOGLE_SEARCH_CX']; + if (!apiKey) throw new Error('GOOGLE_SEARCH_API_KEY not configured'); + if (!cx) throw new Error('GOOGLE_SEARCH_CX not configured'); + + const params = new URLSearchParams({ + key: apiKey, + cx, + q: query, + num: String(Math.min(maxResults, 10)), + }); + let res: Response; + try { + res = await proxyFetch(`https://www.googleapis.com/customsearch/v1?${params}`); + } catch (err: unknown) { + throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); + } + + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + + const data = (await res.json()) as { + items?: Array<{ title: string; link: string; snippet: string; pagemap?: { metatags?: Array<{ 'article:published_time'?: string }> } }>; + }; + + return (data.items ?? []).slice(0, maxResults).map(r => ({ + title: r.title, + url: r.link, + snippet: r.snippet, + date: r.pagemap?.metatags?.[0]?.['article:published_time'], + })); +} + +// ── SerpAPI backend ────────────────────────────────────────────────────── + +async function searchSerpApi(query: string, maxResults: number): Promise { + const apiKey = process.env['SERPAPI_API_KEY']; + if (!apiKey) throw new Error('SERPAPI_API_KEY not configured'); + + const params = new URLSearchParams({ + api_key: apiKey, + q: query, + engine: 'google', + num: String(maxResults), + }); + let res: Response; + try { + res = await proxyFetch(`https://serpapi.com/search.json?${params}`); + } catch (err: unknown) { + throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); + } + + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + + const data = (await res.json()) as { + organic_results?: Array<{ title: string; link: string; snippet: string; date?: string }>; + }; + + return (data.organic_results ?? []).slice(0, maxResults).map(r => ({ + title: r.title, + url: r.link, + snippet: r.snippet, + date: r.date, + })); +} + // ── Brave Search backend ─────────────────────────────────────────────────── async function searchBrave(query: string, maxResults: number): Promise { @@ -186,6 +327,45 @@ async function searchBrave(query: string, maxResults: number): Promise { + const apiKey = process.env['EXA_API_KEY']; + if (!apiKey) throw new Error('EXA_API_KEY not configured'); + + let res: Response; + try { + res = await proxyFetch('https://api.exa.ai/search', { + method: 'POST', + headers: { + 'x-api-key': apiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + numResults: maxResults, + type: 'auto', + contents: { text: { maxCharacters: 300 } }, + }), + }); + } catch (err: unknown) { + throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); + } + + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + + const data = (await res.json()) as { + results?: Array<{ title: string; url: string; text?: string; publishedDate?: string }>; + }; + + return (data.results ?? []).slice(0, maxResults).map(r => ({ + title: r.title, + url: r.url, + snippet: r.text ?? '', + date: r.publishedDate, + })); +} + // ── Bocha (博查) backend ──────────────────────────────────────────────────── async function searchBocha(query: string, maxResults: number): Promise { diff --git a/packages/org-manager/src/api-server.ts b/packages/org-manager/src/api-server.ts index b5491064..06c1be88 100644 --- a/packages/org-manager/src/api-server.ts +++ b/packages/org-manager/src/api-server.ts @@ -7306,7 +7306,12 @@ EXPLANATION_END`; const mask = (key?: string) => key ? '***' + key.slice(-4) : ''; this.json(res, 200, { serper: { configured: !!search.serperApiKey || !!process.env['SERPER_API_KEY'], preview: mask(search.serperApiKey || process.env['SERPER_API_KEY']) }, + tavily: { configured: !!search.tavilyApiKey || !!process.env['TAVILY_API_KEY'], preview: mask(search.tavilyApiKey || process.env['TAVILY_API_KEY']) }, + bing: { configured: !!search.bingApiKey || !!process.env['BING_SEARCH_API_KEY'], preview: mask(search.bingApiKey || process.env['BING_SEARCH_API_KEY']) }, + google: { configured: !!(search.googleSearchApiKey && search.googleSearchCx) || !!(process.env['GOOGLE_SEARCH_API_KEY'] && process.env['GOOGLE_SEARCH_CX']), preview: mask(search.googleSearchApiKey || process.env['GOOGLE_SEARCH_API_KEY']) }, + serpapi: { configured: !!search.serpApiKey || !!process.env['SERPAPI_API_KEY'], preview: mask(search.serpApiKey || process.env['SERPAPI_API_KEY']) }, brave: { configured: !!search.braveApiKey || !!process.env['BRAVE_SEARCH_API_KEY'], preview: mask(search.braveApiKey || process.env['BRAVE_SEARCH_API_KEY']) }, + exa: { configured: !!search.exaApiKey || !!process.env['EXA_API_KEY'], preview: mask(search.exaApiKey || process.env['EXA_API_KEY']) }, bocha: { configured: !!search.bochaApiKey || !!process.env['BOCHA_API_KEY'], preview: mask(search.bochaApiKey || process.env['BOCHA_API_KEY']) }, }); return; @@ -7322,11 +7327,41 @@ EXPLANATION_END`; if (body['serperApiKey']) process.env['SERPER_API_KEY'] = body['serperApiKey'] as string; else delete process.env['SERPER_API_KEY']; } + if (typeof body['tavilyApiKey'] === 'string') { + updates.tavilyApiKey = body['tavilyApiKey'] || undefined; + if (body['tavilyApiKey']) process.env['TAVILY_API_KEY'] = body['tavilyApiKey'] as string; + else delete process.env['TAVILY_API_KEY']; + } + if (typeof body['bingApiKey'] === 'string') { + updates.bingApiKey = body['bingApiKey'] || undefined; + if (body['bingApiKey']) process.env['BING_SEARCH_API_KEY'] = body['bingApiKey'] as string; + else delete process.env['BING_SEARCH_API_KEY']; + } + if (typeof body['googleSearchApiKey'] === 'string') { + updates.googleSearchApiKey = body['googleSearchApiKey'] || undefined; + if (body['googleSearchApiKey']) process.env['GOOGLE_SEARCH_API_KEY'] = body['googleSearchApiKey'] as string; + else delete process.env['GOOGLE_SEARCH_API_KEY']; + } + if (typeof body['googleSearchCx'] === 'string') { + updates.googleSearchCx = body['googleSearchCx'] || undefined; + if (body['googleSearchCx']) process.env['GOOGLE_SEARCH_CX'] = body['googleSearchCx'] as string; + else delete process.env['GOOGLE_SEARCH_CX']; + } + if (typeof body['serpApiKey'] === 'string') { + updates.serpApiKey = body['serpApiKey'] || undefined; + if (body['serpApiKey']) process.env['SERPAPI_API_KEY'] = body['serpApiKey'] as string; + else delete process.env['SERPAPI_API_KEY']; + } if (typeof body['braveApiKey'] === 'string') { updates.braveApiKey = body['braveApiKey'] || undefined; if (body['braveApiKey']) process.env['BRAVE_SEARCH_API_KEY'] = body['braveApiKey'] as string; else delete process.env['BRAVE_SEARCH_API_KEY']; } + if (typeof body['exaApiKey'] === 'string') { + updates.exaApiKey = body['exaApiKey'] || undefined; + if (body['exaApiKey']) process.env['EXA_API_KEY'] = body['exaApiKey'] as string; + else delete process.env['EXA_API_KEY']; + } if (typeof body['bochaApiKey'] === 'string') { updates.bochaApiKey = body['bochaApiKey'] || undefined; if (body['bochaApiKey']) process.env['BOCHA_API_KEY'] = body['bochaApiKey'] as string; @@ -7352,7 +7387,12 @@ EXPLANATION_END`; const mask = (key?: string) => key ? '***' + key.slice(-4) : ''; this.json(res, 200, { serper: { configured: !!search.serperApiKey || !!process.env['SERPER_API_KEY'], preview: mask(search.serperApiKey || process.env['SERPER_API_KEY']) }, + tavily: { configured: !!search.tavilyApiKey || !!process.env['TAVILY_API_KEY'], preview: mask(search.tavilyApiKey || process.env['TAVILY_API_KEY']) }, + bing: { configured: !!search.bingApiKey || !!process.env['BING_SEARCH_API_KEY'], preview: mask(search.bingApiKey || process.env['BING_SEARCH_API_KEY']) }, + google: { configured: !!(search.googleSearchApiKey && search.googleSearchCx) || !!(process.env['GOOGLE_SEARCH_API_KEY'] && process.env['GOOGLE_SEARCH_CX']), preview: mask(search.googleSearchApiKey || process.env['GOOGLE_SEARCH_API_KEY']) }, + serpapi: { configured: !!search.serpApiKey || !!process.env['SERPAPI_API_KEY'], preview: mask(search.serpApiKey || process.env['SERPAPI_API_KEY']) }, brave: { configured: !!search.braveApiKey || !!process.env['BRAVE_SEARCH_API_KEY'], preview: mask(search.braveApiKey || process.env['BRAVE_SEARCH_API_KEY']) }, + exa: { configured: !!search.exaApiKey || !!process.env['EXA_API_KEY'], preview: mask(search.exaApiKey || process.env['EXA_API_KEY']) }, bocha: { configured: !!search.bochaApiKey || !!process.env['BOCHA_API_KEY'], preview: mask(search.bochaApiKey || process.env['BOCHA_API_KEY']) }, }); return; diff --git a/packages/shared/src/utils/config.ts b/packages/shared/src/utils/config.ts index 112f9391..77a8e6c8 100644 --- a/packages/shared/src/utils/config.ts +++ b/packages/shared/src/utils/config.ts @@ -76,7 +76,17 @@ export interface MarkusConfig { }; integrations?: { feishu?: { appId?: string; appSecret?: string }; - search?: { serperApiKey?: string; braveApiKey?: string; bochaApiKey?: string }; + search?: { + serperApiKey?: string; + tavilyApiKey?: string; + bingApiKey?: string; + googleSearchApiKey?: string; + googleSearchCx?: string; + serpApiKey?: string; + braveApiKey?: string; + exaApiKey?: string; + bochaApiKey?: string; + }; embedding?: { apiKey?: string }; }; database?: { diff --git a/packages/web-ui/src/api.ts b/packages/web-ui/src/api.ts index 5e11af52..6e6e8336 100644 --- a/packages/web-ui/src/api.ts +++ b/packages/web-ui/src/api.ts @@ -1280,9 +1280,9 @@ export const api = { } }, stopConcurrentBrowserTest: () => request<{ ok: boolean }>('/settings/browser/test-concurrent', { method: 'DELETE' }), - getSearch: () => request<{ serper: { configured: boolean; preview: string }; brave: { configured: boolean; preview: string }; bocha: { configured: boolean; preview: string } }>('/settings/search'), - updateSearch: (keys: { serperApiKey?: string; braveApiKey?: string; bochaApiKey?: string }) => - request<{ serper: { configured: boolean; preview: string }; brave: { configured: boolean; preview: string }; bocha: { configured: boolean; preview: string } }>('/settings/search', { method: 'POST', body: JSON.stringify(keys) }), + getSearch: () => request<{ serper: { configured: boolean; preview: string }; tavily: { configured: boolean; preview: string }; bing: { configured: boolean; preview: string }; google: { configured: boolean; preview: string }; serpapi: { configured: boolean; preview: string }; brave: { configured: boolean; preview: string }; exa: { configured: boolean; preview: string }; bocha: { configured: boolean; preview: string } }>('/settings/search'), + updateSearch: (keys: { serperApiKey?: string; tavilyApiKey?: string; bingApiKey?: string; googleSearchApiKey?: string; googleSearchCx?: string; serpApiKey?: string; braveApiKey?: string; exaApiKey?: string; bochaApiKey?: string }) => + request<{ serper: { configured: boolean; preview: string }; tavily: { configured: boolean; preview: string }; bing: { configured: boolean; preview: string }; google: { configured: boolean; preview: string }; serpapi: { configured: boolean; preview: string }; brave: { configured: boolean; preview: string }; exa: { configured: boolean; preview: string }; bocha: { configured: boolean; preview: string } }>('/settings/search', { method: 'POST', body: JSON.stringify(keys) }), getRemote: () => request('/settings/remote'), enableRemote: () => request<{ ok: boolean; status: RemoteStatus }>('/settings/remote/enable', { method: 'POST' }), disableRemote: () => request<{ ok: boolean }>('/settings/remote/disable', { method: 'POST' }), diff --git a/packages/web-ui/src/components/Onboarding.tsx b/packages/web-ui/src/components/Onboarding.tsx index 74110c86..e1991ede 100644 --- a/packages/web-ui/src/components/Onboarding.tsx +++ b/packages/web-ui/src/components/Onboarding.tsx @@ -58,8 +58,8 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr const envDetected = useRef(false); // Search API key state - const [searchKeys, setSearchKeys] = useState<{ serper: { configured: boolean; preview: string }; brave: { configured: boolean; preview: string }; bocha: { configured: boolean; preview: string } } | null>(null); - const [searchForm, setSearchForm] = useState({ serperApiKey: '', braveApiKey: '', bochaApiKey: '' }); + const [searchKeys, setSearchKeys] = useState<{ serper: { configured: boolean; preview: string }; tavily: { configured: boolean; preview: string }; bing: { configured: boolean; preview: string }; google: { configured: boolean; preview: string }; serpapi: { configured: boolean; preview: string }; brave: { configured: boolean; preview: string }; exa: { configured: boolean; preview: string }; bocha: { configured: boolean; preview: string } } | null>(null); + const [searchForm, setSearchForm] = useState({ serperApiKey: '', tavilyApiKey: '', bingApiKey: '', googleSearchApiKey: '', googleSearchCx: '', serpApiKey: '', braveApiKey: '', exaApiKey: '', bochaApiKey: '' }); const [searchSaving, setSearchSaving] = useState(false); const [searchConfigured, setSearchConfigured] = useState(false); const [searchMsg, setSearchMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); @@ -183,7 +183,7 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr if (res.ok) { const data = await res.json() as typeof searchKeys; setSearchKeys(data); - if (data && (data.serper.configured || data.brave.configured || data.bocha.configured)) { + if (data && (data.serper.configured || data.tavily.configured || data.bing.configured || data.google.configured || data.serpapi.configured || data.brave.configured || data.exa.configured || data.bocha.configured)) { setSearchConfigured(true); } } @@ -191,13 +191,19 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr }; const saveSearchKeys = async () => { - const hasAny = searchForm.serperApiKey || searchForm.braveApiKey || searchForm.bochaApiKey; + const hasAny = searchForm.serperApiKey || searchForm.tavilyApiKey || searchForm.bingApiKey || searchForm.googleSearchApiKey || searchForm.googleSearchCx || searchForm.serpApiKey || searchForm.braveApiKey || searchForm.exaApiKey || searchForm.bochaApiKey; if (!hasAny) return; setSearchSaving(true); setSearchMsg(null); try { const updates: Record = {}; if (searchForm.serperApiKey) updates.serperApiKey = searchForm.serperApiKey; + if (searchForm.tavilyApiKey) updates.tavilyApiKey = searchForm.tavilyApiKey; + if (searchForm.bingApiKey) updates.bingApiKey = searchForm.bingApiKey; + if (searchForm.googleSearchApiKey) updates.googleSearchApiKey = searchForm.googleSearchApiKey; + if (searchForm.googleSearchCx) updates.googleSearchCx = searchForm.googleSearchCx; + if (searchForm.serpApiKey) updates.serpApiKey = searchForm.serpApiKey; if (searchForm.braveApiKey) updates.braveApiKey = searchForm.braveApiKey; + if (searchForm.exaApiKey) updates.exaApiKey = searchForm.exaApiKey; if (searchForm.bochaApiKey) updates.bochaApiKey = searchForm.bochaApiKey; const res = await fetch('/api/settings/search', { method: 'POST', headers: authHeaders(), @@ -207,7 +213,7 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr const data = await res.json() as typeof searchKeys; setSearchKeys(data); setSearchConfigured(true); - setSearchForm({ serperApiKey: '', braveApiKey: '', bochaApiKey: '' }); + setSearchForm({ serperApiKey: '', tavilyApiKey: '', bingApiKey: '', googleSearchApiKey: '', googleSearchCx: '', serpApiKey: '', braveApiKey: '', exaApiKey: '', bochaApiKey: '' }); setSearchMsg({ type: 'ok', text: t('search.saved') }); } else { setSearchMsg({ type: 'err', text: t('search.failedToSave') }); @@ -508,7 +514,12 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr {searchKeys && ([ { id: 'serper' as const, label: t('search.serper') }, + { id: 'tavily' as const, label: t('search.tavily') }, + { id: 'bing' as const, label: t('search.bing') }, + { id: 'google' as const, label: t('search.google') }, + { id: 'serpapi' as const, label: t('search.serpapi') }, { id: 'brave' as const, label: t('search.brave') }, + { id: 'exa' as const, label: t('search.exa') }, { id: 'bocha' as const, label: t('search.bocha') }, ]).filter(item => searchKeys[item.id]?.configured).map(item => (
@@ -525,12 +536,17 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr
{t('search.description')}
- {searchKeys && (searchKeys.serper.configured || searchKeys.brave.configured || searchKeys.bocha.configured) && ( + {searchKeys && (searchKeys.serper.configured || searchKeys.tavily.configured || searchKeys.bing.configured || searchKeys.google.configured || searchKeys.serpapi.configured || searchKeys.brave.configured || searchKeys.exa.configured || searchKeys.bocha.configured) && (
{t('search.detected')}
{([ { id: 'serper' as const, label: t('search.serper') }, + { id: 'tavily' as const, label: t('search.tavily') }, + { id: 'bing' as const, label: t('search.bing') }, + { id: 'google' as const, label: t('search.google') }, + { id: 'serpapi' as const, label: t('search.serpapi') }, { id: 'brave' as const, label: t('search.brave') }, + { id: 'exa' as const, label: t('search.exa') }, { id: 'bocha' as const, label: t('search.bocha') }, ]).filter(item => searchKeys[item.id]?.configured).map(item => (
@@ -547,7 +563,12 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr
{([ { label: t('search.serper'), field: 'serperApiKey' as const }, + { label: t('search.tavily'), field: 'tavilyApiKey' as const }, + { label: t('search.bing'), field: 'bingApiKey' as const }, + { label: t('search.google'), field: 'googleSearchApiKey' as const }, + { label: t('search.serpapi'), field: 'serpApiKey' as const }, { label: t('search.brave'), field: 'braveApiKey' as const }, + { label: t('search.exa'), field: 'exaApiKey' as const }, { label: t('search.bocha'), field: 'bochaApiKey' as const }, ]).map(item => (
@@ -565,7 +586,7 @@ export function Onboarding({ onComplete, theme, onThemeChange, skipProfile }: Pr