feat(gateway): add web search emulation for Anthropic API Key accounts

Inject web search capability for Claude Console (API Key) accounts that
don't natively support Anthropic's web_search tool. When a pure
web_search request is detected, the gateway calls Brave Search or Tavily
API directly and constructs an Anthropic-protocol-compliant SSE/JSON
response without forwarding to upstream.

Backend:
- New `pkg/websearch/` SDK: Brave and Tavily provider implementations
  with io.LimitReader, proxy support, and Redis-based quota tracking
  (Lua atomic INCR + TTL, DECR rollback on failure)
- Global config via `settings.web_search_emulation_config` (JSON) with
  in-process cache + singleflight, input validation, API key merge on
  save, and sanitized API responses
- Channel-level toggle via `channels.features_config` JSONB column
  (DB migration 101)
- Account-level toggle via `accounts.extra.web_search_emulation`
- Request interception in `Forward()` with SSE streaming response
  construction using json.Marshal (no manual string concatenation)
- Manager hot-reload: `RebuildWebSearchManager()` called on config save
  and startup via `SetWebSearchRedisClient()`
- 70 unit tests covering providers, manager, config validation,
  sanitization, tool detection, query extraction, and response building

Frontend:
- Settings → Gateway tab: Web Search Emulation config card with global
  toggle, provider list (add/remove, API key, priority, quota, proxy)
- Channels → Anthropic tab: web search emulation toggle with global
  state linkage (disabled when global off)
- Account Create/Edit modals: web search emulation toggle for API Key
  type with Toggle component
- Full i18n coverage (zh + en)
This commit is contained in:
erio
2026-04-12 00:02:26 +08:00
parent c738cfec93
commit 1b53ffcac7
37 changed files with 3507 additions and 238 deletions

View File

@@ -1836,6 +1836,9 @@ export default {
defaultPerRequestPrice: 'Default per-request price (fallback when no tier matches)',
defaultImagePrice: 'Default image price (fallback when no tier matches)',
platformConfig: 'Platform Configuration',
webSearchEmulation: 'Web Search Emulation',
webSearchEmulationHint: '⚠️ When enabled, all accounts in this channel\'s Anthropic groups will intercept web_search requests. Use with caution.',
webSearchEmulationGlobalDisabled: 'Please enable the global switch first in Settings → Gateway → Web Search Emulation',
basicSettings: 'Basic Settings',
addPlatform: 'Add Platform',
noPlatforms: 'Click "Add Platform" to start configuring the channel',
@@ -2325,7 +2328,10 @@ export default {
anthropic: {
apiKeyPassthrough: 'Auto passthrough (auth only)',
apiKeyPassthroughDesc:
'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.'
'Only applies to Anthropic API Key accounts. When enabled, messages/count_tokens are forwarded in passthrough mode with auth replacement only, while billing/concurrency/audit and safety filtering are preserved. Disable to roll back immediately.',
webSearchEmulation: 'Web Search Emulation',
webSearchEmulationDesc:
'Enable web search emulation for this API Key account. When a pure web_search request is detected, the gateway calls a third-party search API and constructs the response locally.',
},
modelRestriction: 'Model Restriction (Optional)',
modelWhitelist: 'Model Whitelist',
@@ -4358,6 +4364,31 @@ export default {
cchSigning: 'CCH Signing',
cchSigningHint: 'Sign the billing header in forwarded requests with CCH hash. When disabled, the placeholder is preserved.',
},
webSearchEmulation: {
title: 'Web Search Emulation',
description: 'Inject web search capability for Anthropic API Key accounts that don\'t natively support it',
enabled: 'Enable Web Search Emulation',
enabledHint: 'Global switch. When disabled, web search emulation is inactive for all channels and accounts.',
providers: 'Search Providers',
addProvider: 'Add Provider',
providerType: 'Provider Type',
apiKey: 'API Key',
apiKeyPlaceholder: 'Enter API Key',
apiKeyConfigured: 'Configured',
priority: 'Priority',
priorityHint: 'Lower number = higher priority',
quotaLimit: 'Quota Limit',
quotaLimitHint: '0 = unlimited',
quotaRefreshInterval: 'Refresh Interval',
quotaUsed: 'Used',
proxy: 'Proxy',
expiresAt: 'Expires At',
removeProvider: 'Remove',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
noProviders: 'No search providers configured',
},
site: {
title: 'Site Settings',
description: 'Customize site branding',

View File

@@ -1915,6 +1915,9 @@ export default {
defaultPerRequestPrice: '默认单次价格(未命中层级时使用)',
defaultImagePrice: '默认图片价格(未命中层级时使用)',
platformConfig: '平台配置',
webSearchEmulation: 'Web Search 模拟',
webSearchEmulationHint: '⚠️ 开启后该渠道下所有 Anthropic 分组的账号将自动拦截 web_search 请求,请谨慎操作',
webSearchEmulationGlobalDisabled: '请先在系统设置 → 网关 → Web Search 模拟中启用全局开关',
basicSettings: '基础设置',
addPlatform: '添加平台',
noPlatforms: '点击"添加平台"开始配置渠道',
@@ -2472,7 +2475,10 @@ export default {
anthropic: {
apiKeyPassthrough: '自动透传(仅替换认证)',
apiKeyPassthroughDesc:
'仅对 Anthropic API Key 生效。开启后messages/count_tokens 请求将透传上游并仅替换认证,保留计费/并发/审计及必要安全过滤;关闭即可回滚到现有兼容链路。'
'仅对 Anthropic API Key 生效。开启后messages/count_tokens 请求将透传上游并仅替换认证,保留计费/并发/审计及必要安全过滤;关闭即可回滚到现有兼容链路。',
webSearchEmulation: 'Web Search 模拟',
webSearchEmulationDesc:
'为该 API Key 账号启用 web search 模拟。客户端发送纯 web_search 请求时,由网关调用第三方搜索 API 并构造响应返回。',
},
modelRestriction: '模型限制(可选)',
modelWhitelist: '模型白名单',
@@ -4520,6 +4526,31 @@ export default {
cchSigning: 'CCH 签名',
cchSigningHint: '对转发请求的 billing header 进行 CCH 哈希签名。关闭时保留原始占位符。',
},
webSearchEmulation: {
title: 'Web Search 模拟',
description: '为不原生支持搜索的 Anthropic API Key 账号注入 web search 能力',
enabled: '启用 Web Search 模拟',
enabledHint: '全局开关。关闭后所有渠道和账号的 web search 模拟均不生效。',
providers: '搜索服务商',
addProvider: '添加服务商',
providerType: '服务商类型',
apiKey: 'API Key',
apiKeyPlaceholder: '输入 API Key',
apiKeyConfigured: '已配置',
priority: '优先级',
priorityHint: '数值越小优先级越高',
quotaLimit: '配额上限',
quotaLimitHint: '0 表示无限制',
quotaRefreshInterval: '刷新周期',
quotaUsed: '已使用',
proxy: '代理',
expiresAt: '过期时间',
removeProvider: '删除',
daily: '每日',
weekly: '每周',
monthly: '每月',
noProviders: '未配置搜索服务商',
},
site: {
title: '站点设置',
description: '自定义站点品牌',