feat: 重构 WebSearch/WebFetch,新增 Tavily 适配器及 /web-tools 面板

- WebSearch: 默认 Tavily,适配器优先级 WEB_SEARCH_ADAPTER > settings.webSearchAdapter > tavily
- WebFetch: 支持 Tavily /extract 返回 Markdown,移除 domain blacklist 远程检查
- 新增 /web-tools 命令面板(Search/Fetch 双 Tab + 二级配置菜单)
- 新增 settings 字段: webSearchAdapter, webFetchAdapter, tavilyEndpointUrl, braveApiKey, exaApiKey, exaEndpointUrl, webFetchHttpTimeoutMs
- 适配器联动: Tavily/Exa 从 settings 读取 endpoint 和 API key

Co-Authored-By: deepseek-v4-pro <deepseek-ai@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-15 15:54:02 +08:00
parent 2714bbf812
commit 9d845d77b9
10 changed files with 1005 additions and 160 deletions

View File

@@ -1,13 +1,18 @@
/**
* Search adapter factory — selects the appropriate backend by checking
* whether the API base URL points to Anthropic's official endpoint.
* Search adapter factory — selects the appropriate backend.
*
* Priority (highest first):
* 1. WEB_SEARCH_ADAPTER environment variable (explicit override)
* 2. settings.webSearchAdapter (user-configurable via /web-tools)
* 3. Default: tavily
*/
import { isFirstPartyAnthropicBaseUrl } from 'src/utils/model/providers.js'
import { getSettings_DEPRECATED } from 'src/utils/settings/settings.js'
import { ApiSearchAdapter } from './apiAdapter.js'
import { BingSearchAdapter } from './bingAdapter.js'
import { BraveSearchAdapter } from './braveAdapter.js'
import { ExaSearchAdapter } from './exaAdapter.js'
import { TavilySearchAdapter } from './tavilyAdapter.js'
import type { WebSearchAdapter } from './types.js'
export type {
@@ -17,60 +22,53 @@ export type {
WebSearchAdapter,
} from './types.js'
/**
* Check if the current session uses a third-party (non-Anthropic) API provider.
* These providers don't support Anthropic's server_tools (server-side web search),
* so they must fall back to the Bing scraper adapter.
*/
function isThirdPartyProvider(): boolean {
return !!(
process.env.CLAUDE_CODE_USE_OPENAI ||
process.env.CLAUDE_CODE_USE_GEMINI ||
process.env.CLAUDE_CODE_USE_GROK
)
}
export type SearchAdapterKey = 'api' | 'bing' | 'brave' | 'exa' | 'tavily'
let cachedAdapter: WebSearchAdapter | null = null
let cachedAdapterKey: 'api' | 'bing' | 'brave' | 'exa' | null = null
let cachedAdapterKey: SearchAdapterKey | null = null
export function createAdapter(): WebSearchAdapter {
// 1. Explicit env override
const envAdapter = process.env.WEB_SEARCH_ADAPTER
// Priority:
// 1. Explicit env override (WEB_SEARCH_ADAPTER=api|bing|brave)
// 2. Third-party provider (OpenAI/Gemini/Grok) → bing (no server_tools support)
// 3. First-party Anthropic API → api (server-side web search + connector_text)
// 4. Fallback → bing
const adapterKey =
// 2. Settings preference (set via /web-tools panel)
const settingsAdapter = getSettings_DEPRECATED().webSearchAdapter
const adapterKey: SearchAdapterKey =
envAdapter === 'api' ||
envAdapter === 'bing' ||
envAdapter === 'brave' ||
envAdapter === 'exa'
envAdapter === 'exa' ||
envAdapter === 'tavily'
? envAdapter
: isThirdPartyProvider()
? 'bing'
: isFirstPartyAnthropicBaseUrl()
? 'api'
: 'exa'
: settingsAdapter === 'api' ||
settingsAdapter === 'bing' ||
settingsAdapter === 'brave' ||
settingsAdapter === 'exa' ||
settingsAdapter === 'tavily'
? settingsAdapter
: 'tavily' // 3. Default
if (cachedAdapter && cachedAdapterKey === adapterKey) return cachedAdapter
if (adapterKey === 'api') {
cachedAdapter = new ApiSearchAdapter()
cachedAdapterKey = 'api'
return cachedAdapter
}
if (adapterKey === 'brave') {
cachedAdapter = new BraveSearchAdapter()
cachedAdapterKey = 'brave'
return cachedAdapter
}
if (adapterKey === 'exa') {
cachedAdapter = new ExaSearchAdapter()
cachedAdapterKey = 'exa'
return cachedAdapter
switch (adapterKey) {
case 'api':
cachedAdapter = new ApiSearchAdapter()
break
case 'bing':
cachedAdapter = new BingSearchAdapter()
break
case 'brave':
cachedAdapter = new BraveSearchAdapter()
break
case 'exa':
cachedAdapter = new ExaSearchAdapter()
break
case 'tavily':
default:
cachedAdapter = new TavilySearchAdapter()
break
}
cachedAdapter = new BingSearchAdapter()
cachedAdapterKey = 'bing'
cachedAdapterKey = adapterKey
return cachedAdapter
}