2025-12-18 13:50:39 +08:00
< template >
< AppLayout >
2025-12-25 08:41:36 -08:00
< div class = "mx-auto max-w-4xl space-y-6" >
2025-12-18 13:50:39 +08:00
<!-- Loading State -- >
< div v-if = "loading" class="flex items-center justify-center py-12" >
2026-04-21 17:35:12 +08:00
< div
class = "h-8 w-8 animate-spin rounded-full border-b-2 border-primary-600"
> < / div >
2025-12-18 13:50:39 +08:00
< / div >
<!-- Settings Form -- >
2026-03-21 15:03:18 +08:00
< form v-else @submit.prevent ="saveSettings" class = "space-y-6" novalidate >
2026-03-04 16:59:57 +08:00
<!-- Tab Navigation -- >
2026-03-14 20:22:39 +08:00
< div class = "sticky top-0 z-10 overflow-x-auto settings-tabs-scroll" >
2026-03-04 16:59:57 +08:00
< nav class = "settings-tabs" >
< button
v - for = "tab in settingsTabs"
: key = "tab.key"
type = "button"
2026-04-21 17:35:12 +08:00
: class = " [
'settings-tab' ,
activeTab === tab . key && 'settings-tab-active' ,
] "
2026-03-04 16:59:57 +08:00
@ click = "activeTab = tab.key"
>
< span class = "settings-tab-icon" >
< Icon :name = "tab.icon" size = "sm" / >
< / span >
< span > { { t ( ` admin.settings.tabs. ${ tab . key } ` ) } } < / span >
< / button >
< / nav >
< / div >
<!-- Tab : Security — Admin API Key -- >
< div v-show = "activeTab === 'security'" class="space-y-6" >
2026-04-21 17:35:12 +08:00
<!-- Admin API Key Settings -- >
< div class = "card" >
2025-12-25 08:41:36 -08:00
< div
2026-04-21 17:35:12 +08:00
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
2025-12-25 08:41:36 -08:00
>
2026-04-21 17:35:12 +08:00
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.adminApiKey.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.adminApiKey.description" ) } }
< / p >
2025-12-20 15:11:43 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "space-y-4 p-6" >
<!-- Security Warning -- >
2025-12-25 08:41:36 -08:00
< div
2026-04-21 17:35:12 +08:00
class = "rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
2025-12-25 08:41:36 -08:00
>
2026-04-21 17:35:12 +08:00
< div class = "flex items-start" >
< Icon
name = "exclamationTriangle"
size = "md"
class = "mt-0.5 flex-shrink-0 text-amber-500"
/ >
< p class = "ml-3 text-sm text-amber-700 dark:text-amber-300" >
{ { t ( "admin.settings.adminApiKey.securityWarning" ) } }
2026-03-18 16:22:19 +08:00
< / p >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
<!-- Loading State -- >
2026-03-18 16:22:19 +08:00
< div
2026-04-21 17:35:12 +08:00
v - if = "adminApiKeyLoading"
class = "flex items-center gap-2 text-gray-500"
2026-03-18 16:22:19 +08:00
>
2026-04-21 17:35:12 +08:00
< div
class = "h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
> < / div >
{ { t ( "common.loading" ) } }
2026-03-18 16:22:19 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- No Key Configured -- >
< div
v - else - if = "!adminApiKeyExists"
class = "flex items-center justify-between"
>
< span class = "text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.adminApiKey.notConfigured" ) } }
< / span >
2026-03-18 16:22:19 +08:00
< button
type = "button"
2026-04-21 17:35:12 +08:00
@ click = "createAdminApiKey"
: disabled = "adminApiKeyOperating"
2026-03-18 16:22:19 +08:00
class = "btn btn-primary btn-sm"
>
< svg
2026-04-21 17:35:12 +08:00
v - if = "adminApiKeyOperating"
2026-03-18 16:22:19 +08:00
class = "mr-1 h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
2026-04-21 17:35:12 +08:00
{ {
adminApiKeyOperating
? t ( "admin.settings.adminApiKey.creating" )
: t ( "admin.settings.adminApiKey.create" )
} }
2026-03-18 16:22:19 +08:00
< / button >
< / div >
2026-01-11 21:54:52 -08:00
2026-04-21 17:35:12 +08:00
<!-- Key Exists -- >
< div v-else class = "space-y-4" >
< div class = "flex items-center justify-between" >
< div >
< label
class = "mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.adminApiKey.currentKey" ) } }
< / label >
< code
class = "rounded bg-gray-100 px-2 py-1 font-mono text-sm text-gray-900 dark:bg-dark-700 dark:text-gray-100"
>
{ { adminApiKeyMasked } }
< / code >
< / div >
< div class = "flex gap-2" >
< button
type = "button"
@ click = "regenerateAdminApiKey"
: disabled = "adminApiKeyOperating"
class = "btn btn-secondary btn-sm"
>
{ {
adminApiKeyOperating
? t ( "admin.settings.adminApiKey.regenerating" )
: t ( "admin.settings.adminApiKey.regenerate" )
} }
< / button >
< button
type = "button"
@ click = "deleteAdminApiKey"
: disabled = "adminApiKeyOperating"
class = "btn btn-secondary btn-sm text-red-600 hover:text-red-700 dark:text-red-400"
>
{ { t ( "admin.settings.adminApiKey.delete" ) } }
< / button >
< / div >
2026-01-11 21:54:52 -08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Newly Generated Key Display -- >
< div
v - if = "newAdminApiKey"
class = "space-y-3 rounded-lg border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-900/20"
>
< p
class = "text-sm font-medium text-green-700 dark:text-green-300"
>
{ { t ( "admin.settings.adminApiKey.keyWarning" ) } }
2026-01-11 21:54:52 -08:00
< / p >
2026-04-21 17:35:12 +08:00
< div class = "flex items-center gap-2" >
< code
class = "flex-1 select-all break-all rounded border border-green-300 bg-white px-3 py-2 font-mono text-sm dark:border-green-700 dark:bg-dark-800"
>
{ { newAdminApiKey } }
< / code >
< button
type = "button"
@ click = "copyNewKey"
class = "btn btn-primary btn-sm flex-shrink-0"
>
{ { t ( "admin.settings.adminApiKey.copyKey" ) } }
< / button >
< / div >
< p class = "text-xs text-green-600 dark:text-green-400" >
{ { t ( "admin.settings.adminApiKey.usage" ) } }
2026-01-11 21:54:52 -08:00
< / p >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-01-11 21:54:52 -08:00
< / div >
< / div >
2026-04-21 17:35:12 +08:00
<!-- / T a b : S e c u r i t y — A d m i n A P I K e y - - >
2026-03-07 21:45:18 +08:00
2026-04-21 17:35:12 +08:00
<!-- Tab : Gateway -- >
< div v-show = "activeTab === 'gateway'" class="space-y-6" >
<!-- Overload Cooldown ( 529 ) Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.overloadCooldown.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.overloadCooldown.description" ) } }
< / p >
2026-03-07 21:45:18 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "space-y-5 p-6" >
2026-03-07 21:45:18 +08:00
< div
2026-04-21 17:35:12 +08:00
v - if = "overloadCooldownLoading"
class = "flex items-center gap-2 text-gray-500"
2026-03-07 21:45:18 +08:00
>
2026-04-21 17:35:12 +08:00
< div
class = "h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
> < / div >
{ { t ( "common.loading" ) } }
< / div >
2026-03-07 21:45:18 +08:00
2026-04-21 17:35:12 +08:00
< template v-else >
2026-03-07 21:45:18 +08:00
< div class = "flex items-center justify-between" >
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.overloadCooldown.enabled" )
2026-03-07 21:45:18 +08:00
} } < / label >
2026-04-21 17:35:12 +08:00
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.overloadCooldown.enabledHint" ) } }
2026-03-07 21:45:18 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "overloadCooldownForm.enabled" / >
2026-03-07 21:45:18 +08:00
< / div >
2026-03-26 16:43:38 +08:00
2026-04-21 17:35:12 +08:00
< div
v - if = "overloadCooldownForm.enabled"
class = "space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
2026-03-26 16:43:38 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.overloadCooldown.cooldownMinutes" ) } }
< / label >
< input
v - model . number = "overloadCooldownForm.cooldown_minutes"
type = "number"
min = "1"
max = "120"
class = "input w-32"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.overloadCooldown.cooldownMinutesHint" )
} }
2026-03-26 16:43:38 +08:00
< / p >
< / div >
< / div >
< div
2026-04-21 17:35:12 +08:00
class = "flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700"
2026-03-26 16:43:38 +08:00
>
2026-04-21 17:35:12 +08:00
< button
type = "button"
@ click = "saveOverloadCooldownSettings"
: disabled = "overloadCooldownSaving"
class = "btn btn-primary btn-sm"
2026-03-26 16:43:38 +08:00
>
2026-04-21 17:35:12 +08:00
< svg
v - if = "overloadCooldownSaving"
class = "mr-1 h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
2026-03-26 16:43:38 +08:00
>
2026-04-21 17:35:12 +08:00
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
2026-03-26 16:43:38 +08:00
stroke = "currentColor"
2026-04-21 17:35:12 +08:00
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
{ {
overloadCooldownSaving
? t ( "common.saving" )
: t ( "common.save" )
} }
2026-03-26 16:43:38 +08:00
< / button >
< / div >
2026-04-21 17:35:12 +08:00
< / template >
2026-03-10 11:14:17 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-03-10 11:14:17 +08:00
2026-04-21 17:35:12 +08:00
<!-- Stream Timeout Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.streamTimeout.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.streamTimeout.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Loading State -- >
2026-03-10 11:14:17 +08:00
< div
2026-04-21 17:35:12 +08:00
v - if = "streamTimeoutLoading"
class = "flex items-center gap-2 text-gray-500"
2026-03-10 11:14:17 +08:00
>
2026-04-21 17:35:12 +08:00
< div
class = "h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
> < / div >
{ { t ( "common.loading" ) } }
< / div >
< template v-else >
<!-- Enable Stream Timeout -- >
< div class = "flex items-center justify-between" >
< div >
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.streamTimeout.enabled" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.streamTimeout.enabledHint" ) } }
< / p >
< / div >
< Toggle v-model = "streamTimeoutForm.enabled" / >
2026-03-10 11:14:17 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Settings - Only show when enabled -- >
< div
v - if = "streamTimeoutForm.enabled"
class = "space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
2026-03-10 11:14:17 +08:00
<!-- Action -- >
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.streamTimeout.action" ) } }
2026-03-10 11:14:17 +08:00
< / label >
2026-04-21 17:35:12 +08:00
< select
v - model = "streamTimeoutForm.action"
class = "input w-64"
>
< option value = "temp_unsched" >
{ {
t ( "admin.settings.streamTimeout.actionTempUnsched" )
} }
< / option >
< option value = "error" >
{ { t ( "admin.settings.streamTimeout.actionError" ) } }
< / option >
< option value = "none" >
{ { t ( "admin.settings.streamTimeout.actionNone" ) } }
< / option >
< / select >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.streamTimeout.actionHint" ) } }
< / p >
< / div >
<!-- Temp Unsched Minutes ( only show when action is temp _unsched ) -- >
< div v-if = "streamTimeoutForm.action === 'temp_unsched'" >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.streamTimeout.tempUnschedMinutes" ) } }
< / label >
< input
v - model . number = "streamTimeoutForm.temp_unsched_minutes"
type = "number"
min = "1"
max = "60"
class = "input w-32"
2026-03-10 11:14:17 +08:00
/ >
2026-04-21 17:35:12 +08:00
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.streamTimeout.tempUnschedMinutesHint" )
} }
< / p >
2026-03-10 11:14:17 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Threshold Count -- >
2026-03-10 11:14:17 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.streamTimeout.thresholdCount" ) } }
2026-03-10 11:14:17 +08:00
< / label >
2026-04-21 17:35:12 +08:00
< input
v - model . number = "streamTimeoutForm.threshold_count"
type = "number"
min = "1"
max = "10"
class = "input w-32"
2026-03-10 11:14:17 +08:00
/ >
2026-04-21 17:35:12 +08:00
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.streamTimeout.thresholdCountHint" ) } }
< / p >
2026-03-10 11:14:17 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Threshold Window Minutes -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ {
t ( "admin.settings.streamTimeout.thresholdWindowMinutes" )
} }
< / label >
< input
v - model . number = "
streamTimeoutForm . threshold _window _minutes
"
type = "number"
min = "1"
max = "60"
class = "input w-32"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
t (
"admin.settings.streamTimeout.thresholdWindowMinutesHint" ,
)
} }
< / p >
< / div >
2026-03-10 11:14:17 +08:00
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
<!-- Save Button -- >
< div
class = "flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700"
>
< button
type = "button"
@ click = "saveStreamTimeoutSettings"
: disabled = "streamTimeoutSaving"
class = "btn btn-primary btn-sm"
>
< svg
v - if = "streamTimeoutSaving"
class = "mr-1 h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
{ {
streamTimeoutSaving
? t ( "common.saving" )
: t ( "common.save" )
} }
< / button >
< / div >
< / template >
< / div >
< / div >
<!-- Request Rectifier Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.rectifier.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.rectifier.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Loading State -- >
< div
v - if = "rectifierLoading"
class = "flex items-center gap-2 text-gray-500"
>
< div
class = "h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
> < / div >
{ { t ( "common.loading" ) } }
< / div >
< template v-else >
<!-- Master Toggle -- >
< div class = "flex items-center justify-between" >
< div >
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.rectifier.enabled" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.rectifier.enabledHint" ) } }
< / p >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "rectifierForm.enabled" / >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Sub - toggles ( only show when master is enabled ) -- >
< div
v - if = "rectifierForm.enabled"
class = "space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
<!-- Thinking Signature Rectifier -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
> { {
t ( "admin.settings.rectifier.thinkingSignature" )
} } < / l a b e l
>
< p class = "text-xs text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.rectifier.thinkingSignatureHint" )
} }
< / p >
< / div >
< Toggle
v - model = "rectifierForm.thinking_signature_enabled"
/ >
< / div >
<!-- Thinking Budget Rectifier -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
> { {
t ( "admin.settings.rectifier.thinkingBudget" )
} } < / l a b e l
>
< p class = "text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.rectifier.thinkingBudgetHint" ) } }
< / p >
< / div >
< Toggle v-model = "rectifierForm.thinking_budget_enabled" / >
< / div >
<!-- API Key Signature Rectifier -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
> { {
t ( "admin.settings.rectifier.apikeySignature" )
} } < / l a b e l
>
< p class = "text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.rectifier.apikeySignatureHint" ) } }
< / p >
< / div >
< Toggle v-model = "rectifierForm.apikey_signature_enabled" / >
< / div >
<!-- Custom Patterns ( only when apikey _signature _enabled ) -- >
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)
2026-04-12 00:02:26 +08:00
< div
2026-04-21 17:35:12 +08:00
v - if = "rectifierForm.apikey_signature_enabled"
class = "ml-4 space-y-3 border-l-2 border-gray-200 pl-4 dark:border-dark-600"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
> { {
t ( "admin.settings.rectifier.apikeyPatterns" )
} } < / l a b e l
>
< p class = "text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.rectifier.apikeyPatternsHint" ) } }
< / p >
< / div >
< div
v - for = " (
_ , index
) in rectifierForm . apikey _signature _patterns "
: key = "index"
class = "flex items-center gap-2"
>
< input
v - model = "rectifierForm.apikey_signature_patterns[index]"
type = "text"
class = "input input-sm flex-1"
: placeholder = "
t ( 'admin.settings.rectifier.apikeyPatternPlaceholder' )
"
/ >
< button
type = "button"
@ click = "
rectifierForm . apikey _signature _patterns . splice (
index ,
1 ,
)
"
class = "btn btn-ghost btn-xs text-red-500 hover:text-red-700"
>
< svg
class = "h-4 w-4"
fill = "none"
stroke = "currentColor"
viewBox = "0 0 24 24"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M6 18L18 6M6 6l12 12"
/ >
< / svg >
< / button >
< / div >
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)
2026-04-12 00:02:26 +08:00
< button
type = "button"
2026-04-21 17:35:12 +08:00
@ click = "rectifierForm.apikey_signature_patterns.push('')"
class = "btn btn-ghost btn-xs text-primary-600 dark:text-primary-400"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
+ { { t ( "admin.settings.rectifier.addPattern" ) } }
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)
2026-04-12 00:02:26 +08:00
< / button >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
<!-- Save Button -- >
< div
class = "flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700"
>
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)
2026-04-12 00:02:26 +08:00
< button
type = "button"
2026-04-21 17:35:12 +08:00
@ click = "saveRectifierSettings"
: disabled = "rectifierSaving"
class = "btn btn-primary btn-sm"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
< svg
v - if = "rectifierSaving"
class = "mr-1 h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
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)
2026-04-12 00:02:26 +08:00
< / svg >
2026-04-21 17:35:12 +08:00
{ {
rectifierSaving ? t ( "common.saving" ) : t ( "common.save" )
} }
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)
2026-04-12 00:02:26 +08:00
< / button >
2026-04-21 17:35:12 +08:00
< / div >
< / template >
< / div >
< / div >
<!-- Beta Policy Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.betaPolicy.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.betaPolicy.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Loading State -- >
< div
v - if = "betaPolicyLoading"
class = "flex items-center gap-2 text-gray-500"
>
< div
class = "h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
> < / div >
{ { t ( "common.loading" ) } }
< / div >
< template v-else >
<!-- Rule Cards -- >
< div
v - for = "rule in betaPolicyForm.rules"
: key = "rule.beta_token"
class = "rounded-lg border border-gray-200 p-4 dark:border-dark-600"
>
< div class = "mb-3 flex items-center gap-2" >
< span
class = "text-sm font-medium text-gray-900 dark:text-white"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
{ { getBetaDisplayName ( rule . beta _token ) } }
< / span >
< span
class = "rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-dark-700 dark:text-gray-400"
>
{ { rule . beta _token } }
< / span >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "grid grid-cols-2 gap-4" >
<!-- Action -- >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.betaPolicy.action" ) } }
< / label >
< Select
: modelValue = "rule.action"
@ update : modelValue = "rule.action = $event as any"
: options = "betaPolicyActionOptions"
/ >
< / div >
<!-- Scope -- >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.betaPolicy.scope" ) } }
< / label >
< Select
: modelValue = "rule.scope"
@ update : modelValue = "rule.scope = $event as any"
: options = "betaPolicyScopeOptions"
/ >
< / div >
< / div >
<!-- Error Message ( only when action = block ) -- >
< div v-if = "rule.action === 'block'" class="mt-3" >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.betaPolicy.errorMessage" ) } }
< / label >
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)
2026-04-12 00:02:26 +08:00
< input
2026-04-21 17:35:12 +08:00
v - model = "rule.error_message"
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)
2026-04-12 00:02:26 +08:00
type = "text"
class = "input"
2026-04-21 17:35:12 +08:00
: placeholder = "
t ( 'admin.settings.betaPolicy.errorMessagePlaceholder' )
"
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)
2026-04-12 00:02:26 +08:00
/ >
< p class = "mt-1 text-xs text-gray-400 dark:text-gray-500" >
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.betaPolicy.errorMessageHint" ) } }
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)
2026-04-12 00:02:26 +08:00
< / p >
< / div >
2026-03-10 11:14:17 +08:00
2026-04-21 17:35:12 +08:00
<!-- Quick Presets ( only for tokens with presets ) -- >
< div v-if = "betaPresets[rule.beta_token]?.length" class="mt-3" >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.betaPolicy.quickPresets" ) } }
< / label >
< div class = "flex flex-wrap gap-2" >
< button
v - for = "preset in betaPresets[rule.beta_token]"
: key = "preset.label"
type = "button"
class = "inline-flex items-center gap-1 rounded-md border border-primary-200 bg-primary-50 px-2.5 py-1 text-xs font-medium text-primary-700 transition-colors hover:bg-primary-100 dark:border-primary-800 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/50"
@ click = "applyBetaPreset(rule, preset)"
: title = "preset.description"
>
{ { preset . label } }
< / button >
< / div >
< / div >
2026-02-02 22:13:50 +08:00
2026-04-21 17:35:12 +08:00
<!-- Model Whitelist -- >
< div class = "mt-3" >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.betaPolicy.modelWhitelist" ) } }
< / label >
< p class = "mb-2 text-xs text-gray-400 dark:text-gray-500" >
{ { t ( "admin.settings.betaPolicy.modelWhitelistHint" ) } }
< / p >
<!-- Existing patterns -- >
< div
v - for = "(_, index) in rule.model_whitelist || []"
: key = "index"
class = "mb-1.5 flex items-center gap-2"
>
< input
v - model = "rule.model_whitelist![index]"
type = "text"
class = "input input-sm flex-1"
: placeholder = "
t ( 'admin.settings.betaPolicy.modelPatternPlaceholder' )
"
/ >
< button
type = "button"
@ click = "rule.model_whitelist!.splice(index, 1)"
class = "shrink-0 rounded p-1 text-red-400 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M6 18L18 6M6 6l12 12"
/ >
< / svg >
< / button >
< / div >
<!-- Add pattern button -- >
2026-03-02 23:13:39 +08:00
< button
type = "button"
2026-04-21 17:35:12 +08:00
@ click = "
if ( ! rule . model _whitelist ) rule . model _whitelist = [ ] ;
rule . model _whitelist . push ( '' ) ;
"
class = "mb-2 inline-flex items-center gap-1 text-xs text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
2026-03-02 23:13:39 +08:00
>
2026-04-21 17:35:12 +08:00
< svg
class = "h-3.5 w-3.5"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M12 4v16m8-8H4"
/ >
< / svg >
{ { t ( "admin.settings.betaPolicy.addModelPattern" ) } }
2026-03-02 23:13:39 +08:00
< / button >
2026-04-21 17:35:12 +08:00
<!-- Common pattern chips -- >
< div class = "flex flex-wrap items-center gap-1.5" >
< span class = "text-xs text-gray-400 dark:text-gray-500"
> { {
t ( "admin.settings.betaPolicy.commonPatterns" )
} } : < / s p a n
>
< button
v - for = "pattern in commonModelPatterns"
: key = "pattern"
type = "button"
class = "rounded border border-gray-200 px-2 py-0.5 text-xs text-gray-600 transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-700 dark:hover:bg-primary-900/30 dark:hover:text-primary-300"
@ click = "addQuickPattern(rule, pattern)"
>
{ { pattern } }
< / button >
< / div >
< / div >
2026-03-02 23:13:39 +08:00
2026-04-21 17:35:12 +08:00
<!-- Fallback Action ( only when model _whitelist is non - empty ) -- >
2026-03-02 23:13:39 +08:00
< div
2026-04-21 17:35:12 +08:00
v - if = "
rule . model _whitelist && rule . model _whitelist . length > 0
"
class = "mt-3"
2026-03-02 23:13:39 +08:00
>
2026-04-21 17:35:12 +08:00
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.betaPolicy.fallbackAction" ) } }
< / label >
< Select
: modelValue = "rule.fallback_action || 'pass'"
@ update : modelValue = "rule.fallback_action = $event as any"
: options = "betaPolicyActionOptions"
2026-03-02 23:13:39 +08:00
/ >
2026-04-21 17:35:12 +08:00
< p class = "mt-1 text-xs text-gray-400 dark:text-gray-500" >
{ { t ( "admin.settings.betaPolicy.fallbackActionHint" ) } }
< / p >
<!-- Fallback Error Message ( only when fallback _action = block ) -- >
< div v-if = "rule.fallback_action === 'block'" class="mt-2" >
< input
v - model = "rule.fallback_error_message"
type = "text"
class = "input"
: placeholder = "
t (
'admin.settings.betaPolicy.fallbackErrorMessagePlaceholder' ,
)
"
/ >
< p class = "mt-1 text-xs text-gray-400 dark:text-gray-500" >
{ { t ( "admin.settings.betaPolicy.errorMessageHint" ) } }
< / p >
< / div >
2026-03-02 23:13:39 +08:00
< / div >
< / div >
2026-02-02 22:13:50 +08:00
2026-04-21 17:35:12 +08:00
<!-- Save Button -- >
< div
class = "flex justify-end border-t border-gray-100 pt-4 dark:border-dark-700"
2026-02-02 22:13:50 +08:00
>
2026-04-21 17:35:12 +08:00
< button
type = "button"
@ click = "saveBetaPolicySettings"
: disabled = "betaPolicySaving"
class = "btn btn-primary btn-sm"
>
< svg
v - if = "betaPolicySaving"
class = "mr-1 h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
{ {
betaPolicySaving ? t ( "common.saving" ) : t ( "common.save" )
} }
< / button >
< / div >
< / template >
2026-02-02 22:13:50 +08:00
< / div >
2025-12-18 13:50:39 +08:00
< / div >
< / div >
2026-04-21 17:35:12 +08:00
<!-- / T a b : G a t e w a y - - >
2025-12-18 13:50:39 +08:00
2026-04-21 17:35:12 +08:00
<!-- Tab : Security — Registration , Turnstile , LinuxDo -- >
< div v-show = "activeTab === 'security'" class="space-y-6" >
<!-- Registration Settings -- >
< div class = "card" >
2025-12-25 08:41:36 -08:00
< div
2026-04-21 17:35:12 +08:00
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
2025-12-25 08:41:36 -08:00
>
2026-04-21 17:35:12 +08:00
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.registration.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Enable Registration -- >
< div class = "flex items-center justify-between" >
2025-12-18 13:50:39 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.registration.enableRegistration" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.registration.enableRegistrationHint" )
} }
2025-12-18 13:50:39 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.registration_enabled" / >
< / div >
<!-- Email Verification -- >
< div
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
2025-12-18 13:50:39 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.registration.emailVerification" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.emailVerificationHint" ) } }
2025-12-25 08:41:36 -08:00
< / p >
2025-12-18 13:50:39 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.email_verify_enabled" / >
2025-12-18 13:50:39 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Email Suffix Whitelist -- >
< div class = "border-t border-gray-100 pt-4 dark:border-dark-700" >
2026-01-12 09:14:32 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
2026-04-21 17:35:12 +08:00
t ( "admin.settings.registration.emailSuffixWhitelist" )
2026-01-12 09:14:32 +08:00
} } < / label >
2026-04-21 17:35:12 +08:00
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.registration.emailSuffixWhitelistHint" )
} }
< / p >
< div
class = "mt-3 rounded-lg border border-gray-300 bg-white p-2 dark:border-dark-500 dark:bg-dark-700"
>
< div class = "flex flex-wrap items-center gap-2" >
< span
v - for = "suffix in registrationEmailSuffixWhitelistTags"
: key = "suffix"
class = "inline-flex items-center gap-1 rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-700 dark:bg-dark-600 dark:text-gray-200"
>
< span class = "text-gray-400 dark:text-gray-500" > @ < / span >
< span > { { suffix } } < / span >
< button
type = "button"
class = "rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-300 dark:hover:bg-dark-500 dark:hover:text-white"
@ click = "
removeRegistrationEmailSuffixWhitelistTag ( suffix )
"
>
< Icon
name = "x"
size = "xs"
class = "h-3.5 w-3.5"
: stroke - width = "2"
/ >
< / button >
< / span >
< div
class = "flex min-w-[220px] flex-1 items-center gap-1 rounded border border-transparent px-2 py-1 focus-within:border-primary-300 dark:focus-within:border-primary-700"
>
< span
class = "font-mono text-sm text-gray-400 dark:text-gray-500"
> @ < / s p a n
>
< input
v - model = "registrationEmailSuffixWhitelistDraft"
type = "text"
class = "w-full bg-transparent text-sm font-mono text-gray-900 outline-none placeholder:text-gray-400 dark:text-white dark:placeholder:text-gray-500"
: placeholder = "
t (
'admin.settings.registration.emailSuffixWhitelistPlaceholder' ,
)
"
@ input = "
handleRegistrationEmailSuffixWhitelistDraftInput
"
@ keydown = "
handleRegistrationEmailSuffixWhitelistDraftKeydown
"
@ blur = "commitRegistrationEmailSuffixWhitelistDraft"
@ paste = "handleRegistrationEmailSuffixWhitelistPaste"
/ >
< / div >
< / div >
< / div >
< p class = "mt-2 text-xs text-gray-500 dark:text-gray-400" >
{ {
t (
"admin.settings.registration.emailSuffixWhitelistInputHint" ,
)
} }
2026-01-12 09:14:32 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
<!-- Promo Code -- >
< div
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
2026-01-12 09:14:32 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.registration.promoCode" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.promoCodeHint" ) } }
2026-01-12 09:14:32 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.promo_code_enabled" / >
< / div >
2026-01-12 09:14:32 +08:00
2026-04-21 17:35:12 +08:00
<!-- Invitation Code -- >
< div
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
2026-01-12 09:14:32 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.registration.invitationCode" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.invitationCodeHint" ) } }
2026-01-12 09:14:32 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.invitation_code_enabled" / >
< / div >
<!-- Password Reset - Only show when email verification is enabled -- >
< div
v - if = "form.email_verify_enabled"
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
< div >
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.registration.passwordReset" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.passwordResetHint" ) } }
2026-01-12 09:14:32 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.password_reset_enabled" / >
2026-01-12 09:14:32 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Frontend URL - Only show when password reset is enabled -- >
< div
v - if = "form.email_verify_enabled && form.password_reset_enabled"
class = "border-t border-gray-100 pt-4 dark:border-dark-700"
>
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.registration.frontendUrl" ) } }
< / label >
< input
v - model = "form.frontend_url"
type = "url"
class = "input"
: placeholder = "
t ( 'admin.settings.registration.frontendUrlPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.frontendUrlHint" ) } }
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)
2026-04-12 00:02:26 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
<!-- TOTP 2 FA -- >
< div
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.registration.totp" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.registration.totpHint" ) } }
< / p >
<!-- Warning when encryption key not configured -- >
< p
v - if = "!form.totp_encryption_key_configured"
class = "mt-2 text-sm text-amber-600 dark:text-amber-400"
>
{ { t ( "admin.settings.registration.totpKeyNotConfigured" ) } }
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)
2026-04-12 00:02:26 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle
v - model = "form.totp_enabled"
: disabled = "!form.totp_encryption_key_configured"
/ >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< / div >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
<!-- Cloudflare Turnstile Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.turnstile.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.turnstile.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Enable Turnstile -- >
< div class = "flex items-center justify-between" >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.turnstile.enableTurnstile" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.turnstile.enableTurnstileHint" ) } }
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)
2026-04-12 00:02:26 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.turnstile_enabled" / >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
<!-- Turnstile Keys - Only show when enabled -- >
< div
v - if = "form.turnstile_enabled"
class = "border-t border-gray-100 pt-4 dark:border-dark-700"
>
< div class = "grid grid-cols-1 gap-6" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.turnstile.siteKey" ) } }
< / label >
< input
v - model = "form.turnstile_site_key"
type = "text"
class = "input font-mono text-sm"
placeholder = "0x4AAAAAAA..."
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.turnstile.siteKeyHint" ) } }
< a
href = "https://dash.cloudflare.com/"
target = "_blank"
class = "text-primary-600 hover:text-primary-500"
> { {
t ( "admin.settings.turnstile.cloudflareDashboard" )
} } < / a
>
< / p >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
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)
2026-04-12 00:02:26 +08:00
>
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.turnstile.secretKey" ) } }
< / label >
< input
v - model = "form.turnstile_secret_key"
type = "password"
class = "input font-mono text-sm"
placeholder = "0x4AAAAAAA..."
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
form . turnstile _secret _key _configured
? t (
"admin.settings.turnstile.secretKeyConfiguredHint" ,
)
: t ( "admin.settings.turnstile.secretKeyHint" )
} }
< / p >
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)
2026-04-12 00:02:26 +08:00
< / div >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
<!-- LinuxDo Connect OAuth 登录 -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.linuxdo.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.linuxdo.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
< div class = "flex items-center justify-between" >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.linuxdo.enable" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.linuxdo.enableHint" ) } }
< / p >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.linuxdo_connect_enabled" / >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div
v - if = "form.linuxdo_connect_enabled"
class = "border-t border-gray-100 pt-4 dark:border-dark-700"
>
< div class = "grid grid-cols-1 gap-6" >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.linuxdo.clientId" ) } }
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)
2026-04-12 00:02:26 +08:00
< / label >
2026-04-21 17:35:12 +08:00
< input
v - model = "form.linuxdo_connect_client_id"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.linuxdo.clientIdPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.linuxdo.clientIdHint" ) } }
< / p >
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)
2026-04-12 00:02:26 +08:00
< / div >
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.linuxdo.clientSecret" ) } }
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)
2026-04-12 00:02:26 +08:00
< / label >
2026-04-21 17:35:12 +08:00
< input
v - model = "form.linuxdo_connect_client_secret"
type = "password"
class = "input font-mono text-sm"
: placeholder = "
form . linuxdo _connect _client _secret _configured
? t (
'admin.settings.linuxdo.clientSecretConfiguredPlaceholder' ,
)
: t ( 'admin.settings.linuxdo.clientSecretPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
form . linuxdo _connect _client _secret _configured
? t (
"admin.settings.linuxdo.clientSecretConfiguredHint" ,
)
: t ( "admin.settings.linuxdo.clientSecretHint" )
} }
< / p >
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)
2026-04-12 00:02:26 +08:00
< / div >
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.linuxdo.redirectUrl" ) } }
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)
2026-04-12 00:02:26 +08:00
< / label >
2026-04-21 17:35:12 +08:00
< input
v - model = "form.linuxdo_connect_redirect_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.linuxdo.redirectUrlPlaceholder' )
"
/ >
< div
class = "mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
< button
type = "button"
class = "btn btn-secondary btn-sm w-fit"
@ click = "setAndCopyLinuxdoRedirectUrl"
>
{ { t ( "admin.settings.linuxdo.quickSetCopy" ) } }
< / button >
< code
v - if = "linuxdoRedirectUrlSuggestion"
class = "select-all break-all rounded bg-gray-50 px-2 py-1 font-mono text-xs text-gray-600 dark:bg-dark-800 dark:text-gray-300"
>
{ { linuxdoRedirectUrlSuggestion } }
< / code >
< / div >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.linuxdo.redirectUrlHint" ) } }
< / p >
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)
2026-04-12 00:02:26 +08:00
< / div >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
<!-- WeChat Connect OAuth 登录 -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.wechatConnect.title" ) } }
2026-04-21 17:35:12 +08:00
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.wechatConnect.description" ) } }
2026-04-21 17:35:12 +08:00
< / p >
< / div >
< div class = "space-y-5 p-6" >
< div class = "flex items-center justify-between" >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
2026-04-21 22:26:35 +08:00
t ( "admin.settings.wechatConnect.enabledLabel" )
2026-04-21 17:35:12 +08:00
} } < / label >
2026-03-02 03:41:50 +08:00
< p class = "text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.wechatConnect.enabledHint" ) } }
2026-03-02 03:41:50 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle
v - model = "form.wechat_connect_enabled"
data - testid = "wechat-connect-enabled"
/ >
2026-03-02 03:41:50 +08:00
< / div >
< div
2026-04-21 17:35:12 +08:00
v - if = "form.wechat_connect_enabled"
class = "space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700"
2026-03-02 03:41:50 +08:00
>
2026-04-21 07:48:42 -07:00
< div class = "space-y-4" >
< div
class = "rounded-lg border border-gray-200 p-4 dark:border-dark-700"
>
< div class = "flex items-start justify-between gap-4" >
< div >
< h3 class = "font-medium text-gray-900 dark:text-white" >
{ { localText ( "PC 应用" , "PC App" ) } }
< / h3 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ {
localText (
"桌面浏览器通过微信开放平台扫码登录。可与公众号或移动应用同时存在。" ,
"Desktop browsers sign in through WeChat Open Platform QR login. This can coexist with Official Account or Mobile App." ,
)
} }
< / p >
< / div >
< Toggle
: model - value = "form.wechat_connect_open_enabled"
data - testid = "wechat-connect-open-enabled"
@ update : model - value = "handleWeChatOpenEnabledChange"
/ >
< / div >
2026-04-21 20:36:10 +08:00
< div
2026-04-21 07:48:42 -07:00
v - if = "form.wechat_connect_open_enabled"
class = "mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2"
2026-04-21 17:35:12 +08:00
>
2026-04-21 20:36:10 +08:00
< div >
2026-04-21 07:48:42 -07:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { localText ( "PC AppID" , "PC App ID" ) } }
< / label >
< input
v - model = "form.wechat_connect_open_app_id"
2026-04-21 23:26:45 +08:00
data - testid = "wechat-connect-open-app-id"
2026-04-21 07:48:42 -07:00
type = "text"
class = "input font-mono text-sm"
: placeholder = "
localText (
'微信开放平台 PC 应用 AppID' ,
'WeChat Open Platform PC App ID' ,
)
"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-21 20:36:10 +08:00
>
2026-04-21 07:48:42 -07:00
{ { localText ( "PC AppSecret" , "PC App Secret" ) } }
< / label >
< input
v - model = "form.wechat_connect_open_app_secret"
2026-04-21 23:26:45 +08:00
data - testid = "wechat-connect-open-app-secret"
2026-04-21 07:48:42 -07:00
type = "password"
class = "input font-mono text-sm"
: placeholder = "
form . wechat _connect _open _app _secret _configured
? localText (
'密钥已配置,留空以保留当前值。' ,
'Secret configured. Leave empty to keep the current value.' ,
)
: localText (
'微信开放平台 PC 应用 AppSecret' ,
'WeChat Open Platform PC App Secret' ,
)
"
/ >
< / div >
< / div >
< / div >
< div
class = "rounded-lg border border-gray-200 p-4 dark:border-dark-700"
>
< div class = "flex items-start justify-between gap-4" >
< div >
< h3 class = "font-medium text-gray-900 dark:text-white" >
{ { localText ( "公众号" , "Official Account" ) } }
< / h3 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ {
localText (
"仅在微信内浏览器可用;非微信环境下会显示不可用。" ,
"Only available inside the WeChat browser. It is shown as unavailable outside WeChat." ,
)
} }
2026-04-21 20:36:10 +08:00
< / p >
< / div >
< Toggle
2026-04-21 07:48:42 -07:00
: model - value = "form.wechat_connect_mp_enabled"
data - testid = "wechat-connect-mp-enabled"
@ update : model - value = "handleWeChatMPEnabledChange"
2026-04-21 20:36:10 +08:00
/ >
< / div >
< div
2026-04-21 07:48:42 -07:00
v - if = "form.wechat_connect_mp_enabled"
class = "mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2"
2026-04-21 17:35:12 +08:00
>
2026-04-21 20:36:10 +08:00
< div >
2026-04-21 07:48:42 -07:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { localText ( "公众号 AppID" , "Official Account App ID" ) } }
< / label >
< input
v - model = "form.wechat_connect_mp_app_id"
2026-04-21 23:26:45 +08:00
data - testid = "wechat-connect-mp-app-id"
2026-04-21 07:48:42 -07:00
type = "text"
class = "input font-mono text-sm"
: placeholder = "
localText (
'公众号 AppID' ,
'Official Account App ID' ,
)
"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-21 20:36:10 +08:00
>
2026-04-21 07:48:42 -07:00
{ {
localText (
"公众号 AppSecret" ,
"Official Account App Secret" ,
)
} }
< / label >
< input
v - model = "form.wechat_connect_mp_app_secret"
2026-04-21 23:26:45 +08:00
data - testid = "wechat-connect-mp-app-secret"
2026-04-21 07:48:42 -07:00
type = "password"
class = "input font-mono text-sm"
: placeholder = "
form . wechat _connect _mp _app _secret _configured
? localText (
'密钥已配置,留空以保留当前值。' ,
'Secret configured. Leave empty to keep the current value.' ,
)
: localText (
'公众号 AppSecret' ,
'Official Account App Secret' ,
)
"
/ >
< / div >
< / div >
< / div >
< div
class = "rounded-lg border border-gray-200 p-4 dark:border-dark-700"
>
< div class = "flex items-start justify-between gap-4" >
< div >
< h3 class = "font-medium text-gray-900 dark:text-white" >
{ { localText ( "移动应用" , "Mobile App" ) } }
< / h3 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ {
localText (
"原生移动端通过微信 SDK 唤起授权,网页端不会直接发起该流程。" ,
"Native mobile clients start authorization through the WeChat SDK. The web UI does not launch this flow directly." ,
)
} }
2026-04-21 20:36:10 +08:00
< / p >
< / div >
< Toggle
2026-04-21 07:48:42 -07:00
: model - value = "form.wechat_connect_mobile_enabled"
data - testid = "wechat-connect-mobile-enabled"
@ update : model - value = "handleWeChatMobileEnabledChange"
2026-04-21 20:36:10 +08:00
/ >
< / div >
2026-04-21 07:48:42 -07:00
< div
v - if = "form.wechat_connect_mobile_enabled"
class = "mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2"
>
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { localText ( "移动应用 AppID" , "Mobile App ID" ) } }
< / label >
< input
v - model = "form.wechat_connect_mobile_app_id"
2026-04-21 23:26:45 +08:00
data - testid = "wechat-connect-mobile-app-id"
2026-04-21 07:48:42 -07:00
type = "text"
class = "input font-mono text-sm"
: placeholder = "
localText (
'移动应用 AppID' ,
'Mobile App ID' ,
)
"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { localText ( "移动应用 AppSecret" , "Mobile App Secret" ) } }
< / label >
< input
v - model = "form.wechat_connect_mobile_app_secret"
2026-04-21 23:26:45 +08:00
data - testid = "wechat-connect-mobile-app-secret"
2026-04-21 07:48:42 -07:00
type = "password"
class = "input font-mono text-sm"
: placeholder = "
form . wechat _connect _mobile _app _secret _configured
? localText (
'密钥已配置,留空以保留当前值。' ,
'Secret configured. Leave empty to keep the current value.' ,
)
: localText (
'移动应用 AppSecret' ,
'Mobile App Secret' ,
)
"
/ >
< / div >
< / div >
2026-04-20 17:39:57 +08:00
< / div >
2026-04-21 07:48:42 -07:00
< / div >
< div
v - if = "
form . wechat _connect _open _enabled &&
( form . wechat _connect _mp _enabled ||
form . wechat _connect _mobile _enabled )
"
class = "rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-900/40 dark:bg-amber-900/10 dark:text-amber-300"
>
{ {
localText (
"如果同时启用 PC 应用和公众号/移动应用,这些应用需要挂在同一个微信开放平台主体下,否则 UnionID 无法稳定归并账号。" ,
"When PC App is enabled together with Official Account or Mobile App, they should belong to the same WeChat Open Platform account so UnionID can merge identities reliably." ,
)
} }
< / div >
2026-04-20 17:39:57 +08:00
2026-04-21 07:48:42 -07:00
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
2026-04-21 07:48:42 -07:00
{ {
localText (
"浏览器回调地址" ,
"Browser Redirect URL" ,
)
} }
2026-04-21 17:35:12 +08:00
< / label >
< input
data - testid = "wechat-connect-redirect-url"
v - model = "form.wechat_connect_redirect_url"
type = "url"
class = "input font-mono text-sm"
2026-04-21 22:26:35 +08:00
: placeholder = "t('admin.settings.wechatConnect.redirectUrlPlaceholder')"
2026-04-21 17:35:12 +08:00
/ >
2026-04-21 07:48:42 -07:00
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
localText (
"用于 PC 应用和公众号的网页回调。移动应用走原生 SDK 时不直接使用这个浏览器回调。" ,
"Used by PC App and Official Account browser callbacks. Native mobile SDK flows do not start from this browser callback directly." ,
)
} }
< / p >
2026-04-21 17:35:12 +08:00
< div
class = "mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
< button
type = "button"
class = "btn btn-secondary btn-sm w-fit"
@ click = "setAndCopyWeChatRedirectUrl"
>
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.wechatConnect.generateAndCopy" ) } }
2026-04-21 17:35:12 +08:00
< / button >
< code
v - if = "wechatRedirectUrlSuggestion"
class = "select-all break-all rounded bg-gray-50 px-2 py-1 font-mono text-xs text-gray-600 dark:bg-dark-800 dark:text-gray-300"
>
{ { wechatRedirectUrlSuggestion } }
< / code >
2026-04-20 17:39:57 +08:00
< / div >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-20 17:39:57 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-20 17:39:57 +08:00
>
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.wechatConnect.frontendRedirectUrlLabel" ) } }
2026-04-21 17:35:12 +08:00
< / label >
< input
data - testid = "wechat-connect-frontend-redirect-url"
v - model = "form.wechat_connect_frontend_redirect_url"
type = "text"
class = "input font-mono text-sm"
2026-04-21 22:26:35 +08:00
: placeholder = "t('admin.settings.wechatConnect.frontendRedirectUrlPlaceholder')"
2026-04-21 17:35:12 +08:00
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.wechatConnect.frontendRedirectUrlHint" ) } }
2026-04-21 17:35:12 +08:00
< / p >
2026-04-20 17:39:57 +08:00
< / div >
< / div >
< / div >
< / div >
2025-12-18 13:50:39 +08:00
2026-04-21 17:35:12 +08:00
<!-- Generic OIDC OAuth 登录 -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.oidc.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.oidc.description" ) } }
2026-03-20 09:10:01 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< div class = "space-y-5 p-6" >
2026-04-20 17:39:57 +08:00
< div class = "flex items-center justify-between" >
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.oidc.enable" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.oidc.enableHint" ) } }
2026-04-20 17:39:57 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.oidc_connect_enabled" / >
2026-03-03 19:56:27 +08:00
< / div >
2026-04-20 17:39:57 +08:00
2026-04-21 17:35:12 +08:00
< div
v - if = "form.oidc_connect_enabled"
class = "space-y-6 border-t border-gray-100 pt-4 dark:border-dark-700"
>
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-3" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.providerName" ) } }
< / label >
< input
v - model = "form.oidc_connect_provider_name"
type = "text"
class = "input"
: placeholder = "
t ( 'admin.settings.oidc.providerNamePlaceholder' )
"
/ >
< / div >
2026-03-26 10:22:03 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.clientId" ) } }
< / label >
< input
v - model = "form.oidc_connect_client_id"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.clientIdPlaceholder' )
"
/ >
< / div >
2026-03-26 10:22:03 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.clientSecret" ) } }
< / label >
< input
v - model = "form.oidc_connect_client_secret"
type = "password"
class = "input font-mono text-sm"
: placeholder = "
form . oidc _connect _client _secret _configured
? t (
'admin.settings.oidc.clientSecretConfiguredPlaceholder' ,
)
: t ( 'admin.settings.oidc.clientSecretPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
form . oidc _connect _client _secret _configured
? t ( "admin.settings.oidc.clientSecretConfiguredHint" )
: t ( "admin.settings.oidc.clientSecretHint" )
} }
< / p >
< / div >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.issuerUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_issuer_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.issuerUrlPlaceholder' )
"
/ >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.discoveryUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_discovery_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.discoveryUrlPlaceholder' )
"
/ >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.authorizeUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_authorize_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.authorizeUrlPlaceholder' )
"
/ >
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-12 13:11:46 +08:00
>
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.oidc.tokenUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_token_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.tokenUrlPlaceholder' )
"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.userinfoUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_userinfo_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.userinfoUrlPlaceholder' )
"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.jwksUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_jwks_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "t('admin.settings.oidc.jwksUrlPlaceholder')"
2026-04-12 13:11:46 +08:00
/ >
< / div >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.scopes" ) } }
< / label >
< input
v - model = "form.oidc_connect_scopes"
type = "text"
class = "input font-mono text-sm"
: placeholder = "t('admin.settings.oidc.scopesPlaceholder')"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.oidc.scopesHint" ) } }
< / p >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.redirectUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_redirect_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.redirectUrlPlaceholder' )
"
/ >
< div
class = "mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
< button
type = "button"
class = "btn btn-secondary btn-sm w-fit"
@ click = "setAndCopyOIDCRedirectUrl"
>
{ { t ( "admin.settings.oidc.quickSetCopy" ) } }
< / button >
< code
v - if = "oidcRedirectUrlSuggestion"
class = "select-all break-all rounded bg-gray-50 px-2 py-1 font-mono text-xs text-gray-600 dark:bg-dark-800 dark:text-gray-300"
>
{ { oidcRedirectUrlSuggestion } }
< / code >
2026-04-12 13:11:46 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.oidc.redirectUrlHint" ) } }
< / p >
< / div >
< div class = "lg:col-span-2" >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.frontendRedirectUrl" ) } }
< / label >
< input
v - model = "form.oidc_connect_frontend_redirect_url"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.frontendRedirectUrlPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.oidc.frontendRedirectUrlHint" ) } }
< / p >
< / div >
< / div >
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-3" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.tokenAuthMethod" ) } }
< / label >
< select
v - model = "form.oidc_connect_token_auth_method"
class = "input font-mono text-sm"
>
< option value = "client_secret_post" >
client _secret _post
< / option >
< option value = "client_secret_basic" >
client _secret _basic
< / option >
< option value = "none" > none < / option >
< / select >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.clockSkewSeconds" ) } }
< / label >
< input
v - model . number = "form.oidc_connect_clock_skew_seconds"
type = "number"
min = "0"
max = "600"
class = "input"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.allowedSigningAlgs" ) } }
< / label >
< input
v - model = "form.oidc_connect_allowed_signing_algs"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.allowedSigningAlgsPlaceholder' )
"
/ >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-12 13:11:46 +08:00
2026-04-21 17:35:12 +08:00
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-3" >
< div
class = "flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
2026-04-12 13:11:46 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.oidc.usePkce" ) } }
< / label >
2026-04-12 13:11:46 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle
v - model = "form.oidc_connect_use_pkce"
2026-04-22 11:17:32 +08:00
data - testid = "oidc-connect-use-pkce"
2026-04-21 17:35:12 +08:00
/ >
< / div >
< div
class = "flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
2026-04-12 13:11:46 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.oidc.validateIdToken" ) } }
< / label >
2026-04-12 13:11:46 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle
v - model = "form.oidc_connect_validate_id_token"
2026-04-22 11:17:32 +08:00
data - testid = "oidc-connect-validate-id-token"
2026-04-21 17:35:12 +08:00
/ >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-12 13:11:46 +08:00
2026-04-21 17:35:12 +08:00
< div
class = "flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
< div >
< label class = "font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.oidc.requireEmailVerified" ) } }
< / label >
2026-04-12 13:11:46 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle
v - model = "form.oidc_connect_require_email_verified"
/ >
< / div >
< / div >
< div class = "grid grid-cols-1 gap-6 lg:grid-cols-3" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-14 08:03:27 +08:00
>
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.oidc.userinfoEmailPath" ) } }
< / label >
< input
v - model = "form.oidc_connect_userinfo_email_path"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.userinfoEmailPathPlaceholder' )
"
/ >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-04-12 13:11:46 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-12 15:59:45 +08:00
>
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.oidc.userinfoIdPath" ) } }
< / label >
< input
v - model = "form.oidc_connect_userinfo_id_path"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.userinfoIdPathPlaceholder' )
"
/ >
2026-04-12 13:11:46 +08:00
< / div >
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.oidc.userinfoUsernamePath" ) } }
< / label >
< input
v - model = "form.oidc_connect_userinfo_username_path"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.oidc.userinfoUsernamePathPlaceholder' )
"
/ >
< / div >
< / div >
2026-04-12 15:59:45 +08:00
< / div >
< / div >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
<!-- / T a b : S e c u r i t y — R e g i s t r a t i o n , T u r n s t i l e , L i n u x D o , O I D C - - >
2026-04-12 15:59:45 +08:00
2026-04-21 17:35:12 +08:00
<!-- Tab : Users -- >
< div v-show = "activeTab === 'users'" class="space-y-6" >
<!-- Default Settings -- >
< div class = "card" >
2026-03-12 02:42:57 +03:00
< div
2026-04-21 17:35:12 +08:00
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
2026-03-12 02:42:57 +03:00
>
2026-04-21 17:35:12 +08:00
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.defaults.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.defaults.description" ) } }
2025-12-25 08:41:36 -08:00
< / p >
2025-12-18 13:50:39 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "space-y-6 p-6" >
< div class = "grid grid-cols-1 gap-6 md:grid-cols-2" >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.defaults.defaultBalance" ) } }
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)
2026-04-12 00:02:26 +08:00
< / label >
< input
2026-04-21 17:35:12 +08:00
v - model . number = "form.default_balance"
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)
2026-04-12 00:02:26 +08:00
type = "number"
2026-04-21 17:35:12 +08:00
step = "0.01"
min = "0"
class = "input"
placeholder = "0.00"
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)
2026-04-12 00:02:26 +08:00
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.defaults.defaultBalanceHint" ) } }
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)
2026-04-12 00:02:26 +08:00
< / p >
< / div >
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.defaults.defaultConcurrency" ) } }
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)
2026-04-12 00:02:26 +08:00
< / label >
< input
2026-04-21 17:35:12 +08:00
v - model . number = "form.default_concurrency"
type = "number"
min = "1"
class = "input"
placeholder = "1"
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)
2026-04-12 00:02:26 +08:00
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.defaults.defaultConcurrencyHint" ) } }
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)
2026-04-12 00:02:26 +08:00
< / p >
< / div >
2026-04-23 03:33:52 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.defaults.defaultUserRpmLimit" ) } }
< / label >
< input
v - model . number = "form.default_user_rpm_limit"
type = "number"
min = "0"
step = "1"
class = "input"
placeholder = "0"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.defaults.defaultUserRpmLimitHint" ) } }
< / p >
< / div >
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)
2026-04-12 00:02:26 +08:00
< / div >
2026-03-24 10:13:28 +08:00
2026-04-21 17:35:12 +08:00
< div class = "border-t border-gray-100 pt-4 dark:border-dark-700" >
< div class = "mb-3 flex items-center justify-between" >
< div >
< label class = "font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.defaults.defaultSubscriptions" ) } }
< / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.defaults.defaultSubscriptionsHint" )
} }
< / p >
< / div >
< button
type = "button"
class = "btn btn-secondary btn-sm"
@ click = "addDefaultSubscription"
: disabled = "subscriptionGroups.length === 0"
>
{ { t ( "admin.settings.defaults.addDefaultSubscription" ) } }
< / button >
< / div >
< div
v - if = "form.default_subscriptions.length === 0"
class = "rounded border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 dark:border-dark-600 dark:text-gray-400"
>
{ { t ( "admin.settings.defaults.defaultSubscriptionsEmpty" ) } }
< / div >
< div v-else class = "space-y-3" >
< div
v - for = "(item, index) in form.default_subscriptions"
: key = "`default-sub-${index}`"
class = "grid grid-cols-1 gap-3 rounded border border-gray-200 p-3 md:grid-cols-[1fr_160px_auto] dark:border-dark-600"
>
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.defaults.subscriptionGroup" ) } }
< / label >
< Select
v - model = "item.group_id"
class = "default-sub-group-select"
: options = "defaultSubscriptionGroupOptions"
: placeholder = "
t ( 'admin.settings.defaults.subscriptionGroup' )
"
>
< template # selected = "{ option }" >
< GroupBadge
v - if = "option"
: name = "
(
option as unknown as DefaultSubscriptionGroupOption
) . label
"
: platform = "
(
option as unknown as DefaultSubscriptionGroupOption
) . platform
"
: subscription - type = "
(
option as unknown as DefaultSubscriptionGroupOption
) . subscriptionType
"
: rate - multiplier = "
(
option as unknown as DefaultSubscriptionGroupOption
) . rate
"
/ >
< span v-else class = "text-gray-400" >
{ { t ( "admin.settings.defaults.subscriptionGroup" ) } }
< / span >
< / template >
< template # option = "{ option, selected }" >
< GroupOptionItem
: name = "
(
option as unknown as DefaultSubscriptionGroupOption
) . label
"
: platform = "
(
option as unknown as DefaultSubscriptionGroupOption
) . platform
"
: subscription - type = "
(
option as unknown as DefaultSubscriptionGroupOption
) . subscriptionType
"
: rate - multiplier = "
(
option as unknown as DefaultSubscriptionGroupOption
) . rate
"
: description = "
(
option as unknown as DefaultSubscriptionGroupOption
) . description
"
: selected = "selected"
/ >
< / template >
< / Select >
< / div >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ {
t ( "admin.settings.defaults.subscriptionValidityDays" )
} }
< / label >
< input
v - model . number = "item.validity_days"
type = "number"
min = "1"
max = "36500"
class = "input h-[42px]"
/ >
< / div >
< div class = "flex items-end" >
< button
type = "button"
class = "btn btn-secondary default-sub-delete-btn w-full text-red-600 hover:text-red-700 dark:text-red-400"
@ click = "removeDefaultSubscription(index)"
>
{ { t ( "common.delete" ) } }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.title" ) } }
2026-04-21 17:35:12 +08:00
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.description" ) } }
2026-04-21 17:35:12 +08:00
< / p >
< / div >
< div class = "space-y-6 p-6" >
< div
class = "flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
< div >
< label class = "font-medium text-gray-900 dark:text-white" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.requireEmailLabel" ) } }
2026-04-21 17:35:12 +08:00
< / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.requireEmailHint" ) } }
2026-04-21 17:35:12 +08:00
< / p >
< / div >
< Toggle v-model = "form.force_email_on_third_party_signup" / >
< / div >
2026-04-21 20:36:10 +08:00
< div class = "space-y-4" >
2026-04-21 17:35:12 +08:00
< div
v - for = "authSource in authSourceDefaultsMeta"
: key = "authSource.source"
class = "rounded-xl border border-gray-200 p-4 dark:border-dark-700"
>
2026-04-21 20:36:10 +08:00
< div class = "flex items-center justify-between gap-4" >
2026-04-21 17:35:12 +08:00
< div >
2026-04-21 20:36:10 +08:00
< div class = "font-medium text-gray-900 dark:text-white" >
{ { authSource . title } }
< / div >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { authSource . description } }
< / p >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-21 20:36:10 +08:00
< Toggle
v - model = "
authSourceDefaults [ authSource . source ] . grant _on _signup
"
: data - testid = "`auth-source-${authSource.source}-enabled`"
/ >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-21 20:36:10 +08:00
< div
v - if = "authSourceDefaults[authSource.source].grant_on_signup"
: data - testid = "`auth-source-${authSource.source}-panel`"
class = "mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-dark-700"
>
< p class = "text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.enabledHint" ) } }
2026-04-21 20:36:10 +08:00
< / p >
< div class = "grid grid-cols-1 gap-4 md:grid-cols-2" >
2026-04-21 17:35:12 +08:00
< div >
< label
2026-04-21 20:36:10 +08:00
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-21 17:35:12 +08:00
>
2026-04-21 20:36:10 +08:00
{ { t ( "admin.settings.defaults.defaultBalance" ) } }
2026-04-21 17:35:12 +08:00
< / label >
2026-04-21 20:36:10 +08:00
< input
v - model . number = "
authSourceDefaults [ authSource . source ] . balance
"
type = "number"
step = "0.01"
min = "0"
class = "input"
placeholder = "0.00"
/ >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
2026-04-21 17:35:12 +08:00
>
2026-04-21 20:36:10 +08:00
{ { t ( "admin.settings.defaults.defaultConcurrency" ) } }
< / label >
< input
v - model . number = "
authSourceDefaults [ authSource . source ] . concurrency
"
type = "number"
min = "1"
class = "input"
placeholder = "5"
/ >
2026-04-21 17:35:12 +08:00
< / div >
< / div >
< div
class = "flex items-center justify-between rounded border border-gray-200 px-4 py-3 dark:border-dark-700"
>
< div >
< label
class = "font-medium text-gray-900 dark:text-white"
>
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.grantOnFirstBindLabel" ) } }
2026-04-21 17:35:12 +08:00
< / label >
< p
class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400"
>
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.grantOnFirstBindHint" ) } }
2026-04-21 17:35:12 +08:00
< / p >
< / div >
< Toggle
v - model = "
authSourceDefaults [ authSource . source ]
. grant _on _first _bind
"
/ >
< / div >
< div class = "mb-3 flex items-center justify-between" >
< div >
< label
class = "font-medium text-gray-900 dark:text-white"
>
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.defaultSubscriptionsLabel" ) } }
2026-04-21 17:35:12 +08:00
< / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.defaultSubscriptionsHint" ) } }
2026-04-21 17:35:12 +08:00
< / p >
< / div >
< button
type = "button"
class = "btn btn-secondary btn-sm"
@ click = "
addAuthSourceDefaultSubscription ( authSource . source )
"
: disabled = "subscriptionGroups.length === 0"
>
{ {
t ( "admin.settings.defaults.addDefaultSubscription" )
} }
< / button >
< / div >
< div
v - if = "
authSourceDefaults [ authSource . source ] . subscriptions
. length === 0
"
class = "rounded border border-dashed border-gray-300 px-4 py-3 text-sm text-gray-500 dark:border-dark-600 dark:text-gray-400"
>
2026-04-21 22:26:35 +08:00
{ { t ( "admin.settings.authSourceDefaults.noSourceSubscriptions" ) } }
2026-04-21 17:35:12 +08:00
< / div >
< div v-else class = "space-y-3" >
< div
v - for = " ( item , index ) in authSourceDefaults [
authSource . source
] . subscriptions "
: key = "`${authSource.source}-sub-${index}`"
class = "grid grid-cols-1 gap-3 rounded border border-gray-200 p-3 md:grid-cols-[1fr_160px_auto] dark:border-dark-600"
>
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.defaults.subscriptionGroup" ) } }
< / label >
< Select
v - model = "item.group_id"
class = "default-sub-group-select"
: options = "defaultSubscriptionGroupOptions"
: placeholder = "
t ( 'admin.settings.defaults.subscriptionGroup' )
"
>
< template # selected = "{ option }" >
< GroupBadge
v - if = "option"
: name = "
(
option as unknown as DefaultSubscriptionGroupOption
) . label
"
: platform = "
(
option as unknown as DefaultSubscriptionGroupOption
) . platform
"
: subscription - type = "
(
option as unknown as DefaultSubscriptionGroupOption
) . subscriptionType
"
: rate - multiplier = "
(
option as unknown as DefaultSubscriptionGroupOption
) . rate
"
/ >
< span v-else class = "text-gray-400" >
{ {
t ( "admin.settings.defaults.subscriptionGroup" )
} }
< / span >
< / template >
< template # option = "{ option, selected }" >
< GroupOptionItem
: name = "
(
option as unknown as DefaultSubscriptionGroupOption
) . label
"
: platform = "
(
option as unknown as DefaultSubscriptionGroupOption
) . platform
"
: subscription - type = "
(
option as unknown as DefaultSubscriptionGroupOption
) . subscriptionType
"
: rate - multiplier = "
(
option as unknown as DefaultSubscriptionGroupOption
) . rate
"
: description = "
(
option as unknown as DefaultSubscriptionGroupOption
) . description
"
: selected = "selected"
/ >
< / template >
< / Select >
< / div >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ {
t (
"admin.settings.defaults.subscriptionValidityDays" ,
)
} }
< / label >
< input
v - model . number = "item.validity_days"
type = "number"
min = "1"
max = "36500"
class = "input h-[42px]"
/ >
< / div >
< div class = "flex items-end" >
< button
type = "button"
class = "btn btn-secondary w-full text-red-600 hover:text-red-700 dark:text-red-400"
@ click = "
removeAuthSourceDefaultSubscription (
authSource . source ,
index ,
)
"
>
{ { t ( "common.delete" ) } }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- / T a b : U s e r s - - >
<!-- Tab : Gateway — Claude Code , Scheduling -- >
< div v-show = "activeTab === 'gateway'" class="space-y-6" >
<!-- Claude Code Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.claudeCode.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.claudeCode.description" ) } }
< / p >
< / div >
< div class = "p-6" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.claudeCode.minVersion" ) } }
< / label >
< input
v - model = "form.min_claude_code_version"
type = "text"
class = "input max-w-xs font-mono text-sm"
: placeholder = "
t ( 'admin.settings.claudeCode.minVersionPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.claudeCode.minVersionHint" ) } }
< / p >
< / div >
< div class = "mt-4" >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.claudeCode.maxVersion" ) } }
< / label >
< input
v - model = "form.max_claude_code_version"
type = "text"
class = "input max-w-xs font-mono text-sm"
: placeholder = "
t ( 'admin.settings.claudeCode.maxVersionPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.claudeCode.maxVersionHint" ) } }
< / p >
< / div >
< / div >
< / div >
<!-- Gateway Scheduling Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.scheduling.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.scheduling.description" ) } }
< / p >
< / div >
2026-04-21 22:38:47 +08:00
< div class = "space-y-5 p-6" >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.scheduling.allowUngroupedKey" ) } }
2026-04-21 17:35:12 +08:00
< / label >
2026-04-21 22:38:47 +08:00
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.scheduling.allowUngroupedKeyHint" ) } }
< / p >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-21 22:38:47 +08:00
< Toggle v-model = "form.allow_ungrouped_key_scheduling" / >
< / div >
2026-04-21 17:35:12 +08:00
2026-04-21 22:38:47 +08:00
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.openaiExperimentalScheduler.title" ) } }
< / label >
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
t ( "admin.settings.openaiExperimentalScheduler.description" )
} }
< / p >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-21 22:38:47 +08:00
< Toggle v-model = "form.openai_advanced_scheduler_enabled" / >
2026-04-21 17:35:12 +08:00
< / div >
< / div >
< / div >
<!-- Gateway Forwarding Behavior -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.gatewayForwarding.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.gatewayForwarding.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Fingerprint Unification -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ {
t (
"admin.settings.gatewayForwarding.fingerprintUnification" ,
)
} }
< / label >
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
t (
"admin.settings.gatewayForwarding.fingerprintUnificationHint" ,
)
} }
< / p >
< / div >
< Toggle v-model = "form.enable_fingerprint_unification" / >
< / div >
<!-- Metadata Passthrough -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ {
t ( "admin.settings.gatewayForwarding.metadataPassthrough" )
} }
< / label >
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
t (
"admin.settings.gatewayForwarding.metadataPassthroughHint" ,
)
} }
< / p >
< / div >
< Toggle v-model = "form.enable_metadata_passthrough" / >
< / div >
<!-- CCH Signing -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.gatewayForwarding.cchSigning" ) } }
< / label >
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.gatewayForwarding.cchSigningHint" ) } }
< / p >
< / div >
< Toggle v-model = "form.enable_cch_signing" / >
< / div >
< / div >
< / div >
<!-- Web Search Emulation -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.webSearchEmulation.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.webSearchEmulation.description" ) } }
< / p >
< / div >
< div class = "space-y-5 p-6" >
<!-- Global Toggle -- >
< div class = "flex items-center justify-between" >
< div >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.webSearchEmulation.enabled" ) } }
< / label >
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.webSearchEmulation.enabledHint" ) } }
< / p >
< / div >
< Toggle v-model = "webSearchConfig.enabled" / >
< / div >
<!-- Providers -- >
< div v-if = "webSearchConfig.enabled" class="space-y-4" >
< div class = "flex items-center justify-between" >
< label
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.webSearchEmulation.providers" ) } }
< / label >
< button
type = "button"
class = "btn btn-secondary btn-sm"
@ click = "addWebSearchProvider"
>
{ { t ( "admin.settings.webSearchEmulation.addProvider" ) } }
< / button >
< / div >
< div
v - if = "webSearchConfig.providers.length === 0"
class = "rounded-lg border border-dashed border-gray-300 p-4 text-center text-sm text-gray-400 dark:border-dark-600"
>
{ { t ( "admin.settings.webSearchEmulation.noProviders" ) } }
< / div >
< div
v - for = "(provider, pIdx) in webSearchConfig.providers"
: key = "pIdx"
class = "rounded-lg border border-gray-200 dark:border-dark-600"
>
<!-- Collapsible header -- >
< div
class = "flex cursor-pointer items-center justify-between px-4 py-3"
@ click = "toggleProviderExpand(pIdx)"
>
< div class = "flex items-center gap-3" >
< svg
class = "h-4 w-4 text-gray-400 transition-transform"
: class = "{ 'rotate-90': expandedProviders[pIdx] }"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M9 5l7 7-7 7"
/ >
< / svg >
< Select
v - model = "provider.type"
: options = " [
{ value : 'brave' , label : 'Brave Search' } ,
{ value : 'tavily' , label : 'Tavily' } ,
] "
class = "w-36"
@ click . stop
/ >
<!-- Quota summary ( always visible ) -- >
< span class = "text-xs text-gray-400" >
{ { provider . quota _used ? ? 0 } } /
{ {
provider . quota _limit != null &&
provider . quota _limit > 0
? provider . quota _limit
: "∞"
} }
< / span >
< span
v - if = "
! expandedProviders [ pIdx ] &&
provider . api _key _configured
"
class = "text-xs text-green-500"
>
{ {
t (
"admin.settings.webSearchEmulation.apiKeyConfigured" ,
)
} }
< / span >
< / div >
< button
type = "button"
class = "text-red-500 hover:text-red-700 text-xs"
@ click . stop = "removeWebSearchProvider(pIdx)"
>
{ {
t ( "admin.settings.webSearchEmulation.removeProvider" )
} }
< / button >
< / div >
<!-- Expanded content -- >
< div
v - if = "expandedProviders[pIdx]"
class = "space-y-3 border-t border-gray-100 px-4 pb-4 pt-3 dark:border-dark-700"
>
<!-- API Key with inline show / copy -- >
< div >
< label class = "text-xs text-gray-500" > { {
t ( "admin.settings.webSearchEmulation.apiKey" )
} } < / label >
< div class = "relative" >
< input
v - model = "provider.api_key"
: type = "apiKeyVisible[pIdx] ? 'text' : 'password'"
class = "input w-full text-sm"
: class = "
provider . api _key || provider . api _key _configured
? 'pr-16'
: ''
"
: placeholder = "
provider . api _key _configured
? '••••••••'
: t (
'admin.settings.webSearchEmulation.apiKeyPlaceholder' ,
)
"
/ >
< div
v - if = "provider.api_key || provider.api_key_configured"
class = "absolute inset-y-0 right-0 flex items-center pr-1.5"
>
< button
type = "button"
class = "rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
: title = "
apiKeyVisible [ pIdx ]
? t (
'admin.settings.webSearchEmulation.hideApiKey' ,
)
: t (
'admin.settings.webSearchEmulation.showApiKey' ,
)
"
@ click = "apiKeyVisible[pIdx] = !apiKeyVisible[pIdx]"
>
< svg
v - if = "!apiKeyVisible[pIdx]"
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/ >
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/ >
< / svg >
< svg
v - else
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"
/ >
< / svg >
< / button >
< button
type = "button"
class = "rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
: class = " {
'opacity-30 cursor-not-allowed' :
! provider . api _key ,
} "
: title = "
t ( 'admin.settings.webSearchEmulation.copyApiKey' )
"
: disabled = "!provider.api_key"
@ click = "copyApiKey(pIdx)"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/ >
< / svg >
< / button >
< / div >
< / div >
< / div >
<!-- Quota + Subscription in compact row -- >
< div class = "grid grid-cols-2 gap-3" >
< div >
< label class = "text-xs text-gray-500" > { {
t ( "admin.settings.webSearchEmulation.quotaLimit" )
} } < / label >
< input
v - model = "provider.quota_limit"
type = "number"
min = "1"
class = "input text-sm"
: placeholder = "'∞'"
/ >
< p class = "mt-0.5 text-xs text-gray-400" >
{ {
t (
"admin.settings.webSearchEmulation.quotaLimitHint" ,
)
} }
< / p >
< / div >
< div >
< label class = "text-xs text-gray-500" > { {
t ( "admin.settings.webSearchEmulation.subscribedAt" )
} } < / label >
< input
: value = "formatSubscribedAt(provider.subscribed_at)"
type = "date"
class = "input text-sm"
@ input = "
provider . subscribed _at = parseSubscribedAt (
( $event . target as HTMLInputElement ) . value ,
)
"
/ >
< p class = "mt-0.5 text-xs text-gray-400" >
{ {
t (
"admin.settings.webSearchEmulation.subscribedAtHint" ,
)
} }
< / p >
< / div >
< / div >
<!-- Usage display -- >
< div class = "flex items-center gap-2" >
< span class = "text-xs text-gray-500"
> { {
t ( "admin.settings.webSearchEmulation.quotaUsage" )
} } : < / s p a n
>
< div
v - if = "
provider . quota _limit != null &&
provider . quota _limit > 0
"
class = "flex-1 rounded-full bg-gray-200 dark:bg-dark-600"
style = "height: 6px"
>
< div
class = "h-full rounded-full transition-all"
: class = "
quotaPercentage ( provider ) > 90
? 'bg-red-500'
: quotaPercentage ( provider ) > 70
? 'bg-yellow-500'
: 'bg-green-500'
"
: style = " {
width :
Math . min ( quotaPercentage ( provider ) , 100 ) + '%' ,
} "
/ >
< / div >
< div v-else class = "flex-1" / >
< span class = "text-xs text-gray-500"
> { { provider . quota _used ? ? 0 } } /
{ {
provider . quota _limit != null &&
provider . quota _limit > 0
? provider . quota _limit
: "∞"
} } < / s p a n
>
< button
v - if = "(provider.quota_used ?? 0) > 0"
type = "button"
class = "text-xs text-primary-600 hover:text-primary-700"
@ click = "resetWebSearchUsage(pIdx)"
>
{ { t ( "admin.settings.webSearchEmulation.resetUsage" ) } }
< / button >
< / div >
<!-- Proxy + Test on same row -- >
< div class = "flex items-end gap-3" >
< div class = "flex-1" >
< label class = "text-xs text-gray-500" > { {
t ( "admin.settings.webSearchEmulation.proxy" )
} } < / label >
< ProxySelector
v - model = "provider.proxy_id"
: proxies = "webSearchProxies"
/ >
< / div >
< button
type = "button"
class = "btn btn-secondary btn-sm whitespace-nowrap"
@ click = "openTestDialog()"
>
{ { t ( "admin.settings.webSearchEmulation.test" ) } }
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Web Search Test Dialog -- >
< div
v - if = "wsTestDialogOpen"
class = "fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@ click . self = "wsTestDialogOpen = false"
>
< div
class = "mx-4 w-full max-w-lg rounded-xl bg-white p-6 shadow-xl dark:bg-dark-800"
>
< h3
class = "mb-4 text-lg font-semibold text-gray-900 dark:text-white"
>
{ { t ( "admin.settings.webSearchEmulation.testResultTitle" ) } }
< / h3 >
< div class = "flex items-center gap-2" >
< input
v - model = "wsTestQuery"
type = "text"
class = "input flex-1 text-sm"
: placeholder = "
t ( 'admin.settings.webSearchEmulation.testDefaultQuery' )
"
@ keyup . enter = "testWebSearchProvider()"
/ >
< button
type = "button"
class = "btn btn-primary btn-sm"
: disabled = "wsTestLoading"
@ click = "testWebSearchProvider()"
>
{ {
wsTestLoading
? t ( "admin.settings.webSearchEmulation.testing" )
: t ( "admin.settings.webSearchEmulation.test" )
} }
< / button >
< / div >
<!-- Test results -- >
< div
v - if = "wsTestResult"
class = "mt-4 max-h-80 overflow-y-auto rounded-lg bg-gray-50 p-4 dark:bg-dark-700"
>
< p
class = "mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ {
t ( "admin.settings.webSearchEmulation.testResultProvider" )
} } : { { wsTestResult . provider } }
< / p >
< div
v - if = "wsTestResult.results.length === 0"
class = "text-sm text-gray-400"
>
{ { t ( "admin.settings.webSearchEmulation.testNoResults" ) } }
< / div >
< div
v - for = "(r, rIdx) in wsTestResult.results"
: key = "rIdx"
class = "mt-2 border-t border-gray-200 pt-2 first:mt-0 first:border-0 first:pt-0 dark:border-dark-600"
>
< a
: href = "r.url"
target = "_blank"
class = "text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
> { { r . title } } < / a
>
< p class = "mt-0.5 text-xs text-gray-500 dark:text-gray-400" >
{ { r . snippet } }
< / p >
< / div >
< / div >
< div class = "mt-4 flex justify-end" >
< button
type = "button"
class = "btn btn-secondary btn-sm"
@ click = "wsTestDialogOpen = false"
>
{ { t ( "common.close" ) } }
< / button >
< / div >
< / div >
< / div >
< / div >
<!-- / T a b : G a t e w a y — C l a u d e C o d e , S c h e d u l i n g - - >
<!-- Tab : General -- >
< div v-show = "activeTab === 'general'" class="space-y-6" >
<!-- Site Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.site.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.description" ) } }
< / p >
< / div >
< div class = "space-y-6 p-6" >
<!-- Backend Mode -- >
< div
class = "flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
< div >
< h3 class = "text-sm font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.site.backendMode" ) } }
< / h3 >
< p class = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.backendModeDescription" ) } }
< / p >
< / div >
< Toggle v-model = "form.backend_mode_enabled" / >
< / div >
< div class = "grid grid-cols-1 gap-6 md:grid-cols-2" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.siteName" ) } }
< / label >
< input
v - model = "form.site_name"
type = "text"
class = "input"
: placeholder = "t('admin.settings.site.siteNamePlaceholder')"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.siteNameHint" ) } }
< / p >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.siteSubtitle" ) } }
< / label >
< input
v - model = "form.site_subtitle"
type = "text"
class = "input"
: placeholder = "
t ( 'admin.settings.site.siteSubtitlePlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.siteSubtitleHint" ) } }
< / p >
< / div >
< / div >
<!-- API Base URL -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.apiBaseUrl" ) } }
< / label >
< input
v - model = "form.api_base_url"
type = "text"
class = "input font-mono text-sm"
: placeholder = "t('admin.settings.site.apiBaseUrlPlaceholder')"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.apiBaseUrlHint" ) } }
< / p >
< / div >
<!-- Global Table Preferences -- >
< div class = "border-t border-gray-100 pt-4 dark:border-dark-700" >
< h3 class = "text-sm font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.site.tablePreferencesTitle" ) } }
< / h3 >
< p class = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.tablePreferencesDescription" ) } }
< / p >
< div class = "mt-4 grid grid-cols-1 gap-6 md:grid-cols-2" >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.tableDefaultPageSize" ) } }
< / label >
< input
v - model . number = "form.table_default_page_size"
type = "number"
min = "5"
max = "1000"
step = "1"
class = "input w-40"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.tableDefaultPageSizeHint" ) } }
< / p >
< / div >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.tablePageSizeOptions" ) } }
< / label >
< input
v - model = "tablePageSizeOptionsInput"
type = "text"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.site.tablePageSizeOptionsPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.tablePageSizeOptionsHint" ) } }
< / p >
< / div >
< / div >
< / div >
<!-- Custom Endpoints -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.customEndpoints.title" ) } }
< / label >
< p class = "mb-3 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.customEndpoints.description" ) } }
< / p >
< div class = "space-y-3" >
< div
v - for = "(ep, index) in form.custom_endpoints"
: key = "index"
class = "rounded-lg border border-gray-200 p-4 dark:border-dark-600"
>
< div class = "mb-3 flex items-center justify-between" >
< span
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ {
t ( "admin.settings.site.customEndpoints.itemLabel" , {
n : index + 1 ,
} )
} }
< / span >
< button
type = "button"
class = "rounded p-1 text-red-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
@ click = "removeEndpoint(index)"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/ >
< / svg >
< / button >
< / div >
< div class = "grid grid-cols-1 gap-3 sm:grid-cols-2" >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.site.customEndpoints.name" ) } }
< / label >
< input
v - model = "ep.name"
type = "text"
class = "input text-sm"
: placeholder = "
t (
'admin.settings.site.customEndpoints.namePlaceholder' ,
)
"
/ >
< / div >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ {
t ( "admin.settings.site.customEndpoints.endpointUrl" )
} }
< / label >
< input
v - model = "ep.endpoint"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t (
'admin.settings.site.customEndpoints.endpointUrlPlaceholder' ,
)
"
/ >
< / div >
< div class = "sm:col-span-2" >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ {
t (
"admin.settings.site.customEndpoints.descriptionLabel" ,
)
} }
< / label >
< input
v - model = "ep.description"
type = "text"
class = "input text-sm"
: placeholder = "
t (
'admin.settings.site.customEndpoints.descriptionPlaceholder' ,
)
"
/ >
< / div >
< / div >
< / div >
< / div >
< button
type = "button"
class = "mt-3 flex w-full items-center justify-center gap-2 rounded-lg border-2 border-dashed border-gray-300 px-4 py-2.5 text-sm text-gray-500 transition-colors hover:border-primary-400 hover:text-primary-600 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-500 dark:hover:text-primary-400"
@ click = "addEndpoint"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M12 4v16m8-8H4"
/ >
< / svg >
{ { t ( "admin.settings.site.customEndpoints.add" ) } }
< / button >
< / div >
<!-- Contact Info -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.contactInfo" ) } }
< / label >
< input
v - model = "form.contact_info"
type = "text"
class = "input"
: placeholder = "t('admin.settings.site.contactInfoPlaceholder')"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.contactInfoHint" ) } }
< / p >
< / div >
<!-- Doc URL -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.docUrl" ) } }
< / label >
< input
v - model = "form.doc_url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "t('admin.settings.site.docUrlPlaceholder')"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.docUrlHint" ) } }
< / p >
< / div >
<!-- Site Logo Upload -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.siteLogo" ) } }
< / label >
< ImageUpload
v - model = "form.site_logo"
mode = "image"
: upload - label = "t('admin.settings.site.uploadImage')"
: remove - label = "t('admin.settings.site.remove')"
: hint = "t('admin.settings.site.logoHint')"
: max - size = "300 * 1024"
/ >
< / div >
<!-- Home Content -- >
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.site.homeContent" ) } }
< / label >
< textarea
v - model = "form.home_content"
rows = "6"
class = "input font-mono text-sm"
: placeholder = "t('admin.settings.site.homeContentPlaceholder')"
> < / textarea >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.homeContentHint" ) } }
< / p >
<!-- iframe CSP Warning -- >
< p class = "mt-2 text-xs text-amber-600 dark:text-amber-400" >
{ { t ( "admin.settings.site.homeContentIframeWarning" ) } }
< / p >
< / div >
<!-- Hide CCS Import Button -- >
< div
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
< div >
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.site.hideCcsImportButton" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.site.hideCcsImportButtonHint" ) } }
< / p >
< / div >
< Toggle v-model = "form.hide_ccs_import_button" / >
< / div >
< / div >
< / div >
<!-- Custom Menu Items -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.customMenu.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.customMenu.description" ) } }
< / p >
< / div >
< div class = "space-y-4 p-6" >
<!-- Existing menu items -- >
< div
v - for = "(item, index) in form.custom_menu_items"
: key = "item.id || index"
class = "rounded-lg border border-gray-200 p-4 dark:border-dark-600"
>
< div class = "mb-3 flex items-center justify-between" >
< span
class = "text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ {
t ( "admin.settings.customMenu.itemLabel" , { n : index + 1 } )
} }
< / span >
< div class = "flex items-center gap-2" >
<!-- Move up -- >
< button
v - if = "index > 0"
type = "button"
class = "rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dark-700"
: title = "t('admin.settings.customMenu.moveUp')"
@ click = "moveMenuItem(index, -1)"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M5 15l7-7 7 7"
/ >
< / svg >
< / button >
<!-- Move down -- >
< button
v - if = "index < form.custom_menu_items.length - 1"
type = "button"
class = "rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dark-700"
: title = "t('admin.settings.customMenu.moveDown')"
@ click = "moveMenuItem(index, 1)"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M19 9l-7 7-7-7"
/ >
< / svg >
< / button >
<!-- Delete -- >
2026-03-24 10:13:28 +08:00
< button
type = "button"
class = "rounded p-1 text-red-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
2026-04-21 17:35:12 +08:00
: title = "t('admin.settings.customMenu.remove')"
@ click = "removeMenuItem(index)"
2026-03-24 10:13:28 +08:00
>
2026-04-21 17:35:12 +08:00
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/ >
< / svg >
2026-03-24 10:13:28 +08:00
< / button >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
< div class = "grid grid-cols-1 gap-3 sm:grid-cols-2" >
<!-- Label -- >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.customMenu.name" ) } }
< / label >
< input
v - model = "item.label"
type = "text"
class = "input text-sm"
: placeholder = "
t ( 'admin.settings.customMenu.namePlaceholder' )
"
/ >
< / div >
<!-- Visibility -- >
< div >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.customMenu.visibility" ) } }
< / label >
< select v-model = "item.visibility" class="input text-sm" >
< option value = "user" >
{ { t ( "admin.settings.customMenu.visibilityUser" ) } }
< / option >
< option value = "admin" >
{ { t ( "admin.settings.customMenu.visibilityAdmin" ) } }
< / option >
< / select >
< / div >
<!-- URL ( full width ) -- >
< div class = "sm:col-span-2" >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.customMenu.url" ) } }
< / label >
< input
v - model = "item.url"
type = "url"
class = "input font-mono text-sm"
: placeholder = "
t ( 'admin.settings.customMenu.urlPlaceholder' )
"
/ >
< / div >
<!-- SVG Icon ( full width ) -- >
< div class = "sm:col-span-2" >
< label
class = "mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
{ { t ( "admin.settings.customMenu.iconSvg" ) } }
< / label >
< ImageUpload
: model - value = "item.icon_svg"
mode = "svg"
size = "sm"
: upload - label = "t('admin.settings.customMenu.uploadSvg')"
: remove - label = "t('admin.settings.customMenu.removeSvg')"
@ update : model - value = "(v: string) => (item.icon_svg = v)"
/ >
< / div >
< / div >
< / div >
<!-- Add button -- >
< button
type = "button"
class = "flex w-full items-center justify-center gap-2 rounded-lg border-2 border-dashed border-gray-300 py-3 text-sm text-gray-500 transition-colors hover:border-primary-400 hover:text-primary-600 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-500 dark:hover:text-primary-400"
@ click = "addMenuItem"
>
< svg
class = "h-4 w-4"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
stroke - width = "2"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
d = "M12 4v16m8-8H4"
/ >
< / svg >
{ { t ( "admin.settings.customMenu.add" ) } }
< / button >
< / div >
< / div >
< / div >
<!-- / T a b : G e n e r a l - - >
<!-- Tab : Email -- >
<!-- Tab : Payment -- >
< div v-show = "activeTab === 'payment'" class="space-y-6" >
<!-- Payment System Settings -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.payment.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.payment.description" ) } }
< a
2026-04-22 14:57:47 +08:00
: href = "paymentGuideHref"
2026-04-21 17:35:12 +08:00
target = "_blank"
rel = "noopener noreferrer"
class = "ml-2 inline-flex items-center text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
>
< svg
class = "mr-0.5 h-3.5 w-3.5"
fill = "none"
stroke = "currentColor"
viewBox = "0 0 24 24"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/ >
< / svg >
{ { t ( "admin.settings.payment.configGuide" ) } }
< / a >
< / p >
< / div >
< div class = "space-y-4 p-6" >
<!-- Enable toggle -- >
< div class = "flex items-center justify-between" >
< div >
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.payment.enabled" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.payment.enabledHint" ) } }
< / p >
< / div >
< Toggle v-model = "form.payment_enabled" / >
< / div >
< template v-if = "form.payment_enabled" >
<!-- Row 1 : Product name -- >
< div class = "grid grid-cols-3 gap-3" >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.productNamePrefix" )
} } < / l a b e l
> < input
v - model = "form.payment_product_name_prefix"
type = "text"
class = "input"
placeholder = "Sub2API"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.productNameSuffix" )
} } < / l a b e l
> < input
v - model = "form.payment_product_name_suffix"
type = "text"
class = "input"
placeholder = "CNY"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.preview" )
} } < / label >
< div
class = "rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-600 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-300"
>
{ {
( form . payment _product _name _prefix || "Sub2API" ) +
" 100 " +
( form . payment _product _name _suffix || "CNY" )
} }
< / div >
< / div >
< / div >
<!-- Row 2 : Balance toggle + amounts -- >
< div class = "grid grid-cols-2 gap-3 sm:grid-cols-5" >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.minAmount" )
} } < / l a b e l
> < input
: value = "form.payment_min_amount || ''"
@ input = "
form . payment _min _amount =
parseFloat (
( $event . target as HTMLInputElement ) . value ,
) || 0
"
type = "number"
step = "0.01"
min = "0"
class = "input"
: placeholder = "t('admin.settings.payment.noLimit')"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.maxAmount" )
} } < / l a b e l
> < input
: value = "form.payment_max_amount || ''"
@ input = "
form . payment _max _amount =
parseFloat (
( $event . target as HTMLInputElement ) . value ,
) || 0
"
type = "number"
step = "0.01"
min = "0"
class = "input"
: placeholder = "t('admin.settings.payment.noLimit')"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.dailyLimit" )
} } < / l a b e l
> < input
: value = "form.payment_daily_limit || ''"
@ input = "
form . payment _daily _limit =
parseFloat (
( $event . target as HTMLInputElement ) . value ,
) || 0
"
type = "number"
step = "0.01"
min = "0"
class = "input"
: placeholder = "t('admin.settings.payment.noLimit')"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.balanceRechargeMultiplier" )
} } < / label >
< input
: value = "form.payment_balance_recharge_multiplier || ''"
@ input = "
form . payment _balance _recharge _multiplier =
parseFloat (
( $event . target as HTMLInputElement ) . value ,
) || 1
"
type = "number"
step = "0.01"
min = "0.01"
class = "input"
/ >
< p class = "mt-0.5 text-xs text-gray-400" >
{ {
t (
"admin.settings.payment.balanceRechargeMultiplierHint" ,
)
} }
< / p >
< p
class = "mt-1 text-xs font-medium text-primary-600 dark:text-primary-400"
>
{ {
t ( "admin.settings.payment.balanceRechargePreview" , {
usd : (
Number ( form . payment _balance _recharge _multiplier ) ||
1
) . toFixed ( 2 ) ,
} )
} }
< / p >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.rechargeFeeRate" )
} } < / label >
< div class = "relative" >
2026-03-24 10:13:28 +08:00
< input
2026-04-21 17:35:12 +08:00
: value = "form.payment_recharge_fee_rate ?? ''"
@ input = "
form . payment _recharge _fee _rate = Math . min (
100 ,
Math . max (
0 ,
Math . round (
parseFloat (
( $event . target as HTMLInputElement ) . value ||
'0' ,
) * 100 ,
) / 100 ,
) ,
)
"
type = "number"
step = "0.01"
min = "0"
max = "100"
class = "input pr-8"
2026-03-24 10:13:28 +08:00
/ >
2026-04-21 17:35:12 +08:00
< span
class = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400"
> % < / s p a n
>
2026-03-24 10:13:28 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< p class = "mt-0.5 text-xs text-gray-400" >
{ { t ( "admin.settings.payment.rechargeFeeRateHint" ) } }
< / p >
< p
v - if = "(Number(form.payment_recharge_fee_rate) || 0) > 0"
class = "mt-1 text-xs font-medium text-primary-600 dark:text-primary-400"
>
{ {
t ( "admin.settings.payment.rechargeFeePreview" , {
fee : (
Number ( form . payment _recharge _fee _rate ) || 0
) . toFixed ( 2 ) ,
} )
} }
< / p >
< / div >
< div >
< label class = "input-label"
> { { t ( "admin.settings.payment.orderTimeout" ) } }
< span class = "text-red-500" > * < / span > < / l a b e l
> < input
v - model . number = "form.payment_order_timeout_minutes"
type = "number"
min = "1"
class = "input"
required
/ >
< p class = "mt-0.5 text-xs text-gray-400" >
{ { t ( "admin.settings.payment.orderTimeoutHint" ) } }
< / p >
< / div >
< / div >
<!-- Row 3 : Pending orders + load balance + cancel rate limit ( all in one row ) -- >
< div class = "flex flex-wrap items-end gap-4" >
< div class = "w-28" >
< label class = "input-label" > { {
t ( "admin.settings.payment.maxPendingOrders" )
} } < / l a b e l
> < input
v - model . number = "form.payment_max_pending_orders"
type = "number"
min = "1"
class = "input"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.loadBalanceStrategy" )
} } < / label >
< Select
v - model = "form.payment_load_balance_strategy"
: options = "loadBalanceOptions"
class = "w-40"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.cancelRateLimit" )
} } < / label >
< div class = "flex items-center gap-2" >
< button
type = "button"
: class = " [
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2' ,
form . payment _cancel _rate _limit _enabled
? 'bg-primary-500'
: 'bg-gray-300 dark:bg-dark-600' ,
] "
@ click = "
form . payment _cancel _rate _limit _enabled =
! form . payment _cancel _rate _limit _enabled
"
>
< span
: class = " [
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out' ,
form . payment _cancel _rate _limit _enabled
? 'translate-x-5'
: 'translate-x-0' ,
] "
/ >
< / button >
< Select
v - model = "form.payment_cancel_rate_limit_window_mode"
: options = "cancelRateLimitModeOptions"
class = "w-24"
: disabled = "!form.payment_cancel_rate_limit_enabled"
/ >
< span
: class = " [
'text-sm whitespace-nowrap' ,
form . payment _cancel _rate _limit _enabled
? 'text-gray-700 dark:text-gray-300'
: 'text-gray-400 dark:text-gray-600' ,
] "
> { {
t ( "admin.settings.payment.cancelRateLimitEvery" )
} } < / s p a n
>
< input
v - model . number = "form.payment_cancel_rate_limit_window"
type = "number"
min = "1"
required
class = "input w-14 text-center"
: disabled = "!form.payment_cancel_rate_limit_enabled"
/ >
< Select
v - model = "form.payment_cancel_rate_limit_unit"
: options = "cancelRateLimitUnitOptions"
class = "w-28"
: disabled = "!form.payment_cancel_rate_limit_enabled"
/ >
< span
: class = " [
'text-sm whitespace-nowrap' ,
form . payment _cancel _rate _limit _enabled
? 'text-gray-700 dark:text-gray-300'
: 'text-gray-400 dark:text-gray-600' ,
] "
> { {
t ( "admin.settings.payment.cancelRateLimitAllowMax" )
} } < / s p a n
>
2026-03-24 10:13:28 +08:00
< input
2026-04-21 17:35:12 +08:00
v - model . number = "form.payment_cancel_rate_limit_max"
type = "number"
min = "1"
required
class = "input w-14 text-center"
: disabled = "!form.payment_cancel_rate_limit_enabled"
/ >
< span
: class = " [
'text-sm whitespace-nowrap' ,
form . payment _cancel _rate _limit _enabled
? 'text-gray-700 dark:text-gray-300'
: 'text-gray-400 dark:text-gray-600' ,
] "
> { {
t ( "admin.settings.payment.cancelRateLimitTimes" )
} } < / s p a n
>
< / div >
< / div >
< / div >
<!-- Row 4 : Enabled payment types ( provider badges like sub2apipay ) -- >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.enabledPaymentTypes" )
} } < / label >
< div class = "mt-1.5 flex flex-wrap gap-2" >
< button
v - for = "pt in allPaymentTypes"
: key = "pt.value"
type = "button"
@ click = "togglePaymentType(pt.value)"
: class = " [
'rounded-lg border px-3 py-1.5 text-sm font-medium transition-all' ,
isPaymentTypeEnabled ( pt . value )
? 'border-primary-500 bg-primary-500 text-white shadow-sm'
: 'border-gray-300 bg-white text-gray-600 hover:border-gray-400 hover:bg-gray-50 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-300 dark:hover:border-dark-500' ,
] "
>
{ { pt . label } }
< / button >
< / div >
< p class = "mt-2 text-xs text-gray-400 dark:text-gray-500" >
{ { t ( "admin.settings.payment.enabledPaymentTypesHint" ) } }
< a
2026-04-22 18:48:40 +08:00
: href = "paymentMethodsHref"
2026-04-21 17:35:12 +08:00
target = "_blank"
rel = "noopener noreferrer"
class = "ml-1 text-primary-500 hover:text-primary-600 dark:text-primary-400 dark:hover:text-primary-300"
>
{ { t ( "admin.settings.payment.findProvider" ) } }
< svg
class = "mb-0.5 ml-0.5 inline h-3 w-3"
fill = "none"
stroke = "currentColor"
viewBox = "0 0 24 24"
>
< path
stroke - linecap = "round"
stroke - linejoin = "round"
stroke - width = "2"
d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/ >
< / svg >
< / a >
< / p >
< / div >
<!-- Row 5 : Help image + text -- >
< div class = "grid grid-cols-2 gap-3" >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.helpImage" )
} } < / label >
< ImageUpload
v - model = "form.payment_help_image_url"
2026-04-22 07:33:14 -07:00
: upload - label = "t('admin.settings.site.uploadImage')"
: remove - label = "t('admin.settings.site.remove')"
2026-04-21 17:35:12 +08:00
: placeholder = "
t ( 'admin.settings.payment.helpImagePlaceholder' )
"
/ >
< / div >
< div >
< label class = "input-label" > { {
t ( "admin.settings.payment.helpText" )
} } < / label >
< textarea
v - model = "form.payment_help_text"
rows = "3"
class = "input"
: placeholder = "
t ( 'admin.settings.payment.helpTextPlaceholder' )
"
> < / textarea >
< / div >
< / div >
< / template >
2025-12-24 21:30:19 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2025-12-24 21:30:19 +08:00
2026-04-21 17:35:12 +08:00
<!-- Provider Management -- >
< PaymentProviderList
v - if = "form.payment_enabled"
: providers = "providers"
: loading = "providersLoading"
: can - create = "hasAnyPaymentTypeEnabled"
: enabled - payment - types = "form.payment_enabled_types"
: all - payment - types = "allPaymentTypes"
: redirect - label = "t('admin.settings.payment.easypayRedirect')"
@ refresh = "loadProviders"
@ create = "openCreateProvider"
@ edit = "openEditProvider"
@ delete = "confirmDeleteProvider"
@ toggle - field = "handleToggleField"
@ toggle - type = "handleToggleType"
@ reorder = "handleReorderProviders"
/ >
< / div >
2026-01-10 18:37:44 +08:00
2026-04-21 17:35:12 +08:00
< div v-show = "activeTab === 'email'" class="space-y-6" >
<!-- Email disabled hint - show when email _verify _enabled is off -- >
< div v-if = "!form.email_verify_enabled" class="card" >
< div class = "p-6" >
< div class = "flex items-start gap-3" >
< Icon
name = "mail"
size = "md"
class = "mt-0.5 flex-shrink-0 text-gray-400 dark:text-gray-500"
/ >
< div >
< h3 class = "font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.emailTabDisabledTitle" ) } }
< / h3 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.emailTabDisabledHint" ) } }
< / p >
< / div >
< / div >
2026-01-10 18:37:44 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-02-02 22:13:50 +08:00
2026-04-21 17:35:12 +08:00
<!-- SMTP Settings - Only show when email verification is enabled -- >
< div v-if = "form.email_verify_enabled" class="card" >
2026-02-02 22:13:50 +08:00
< div
2026-04-21 17:35:12 +08:00
class = "flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dark-700"
2026-02-02 22:13:50 +08:00
>
< div >
2026-04-21 17:35:12 +08:00
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.smtp.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.smtp.description" ) } }
2026-02-02 22:13:50 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< button
type = "button"
@ click = "testSmtpConnection"
: disabled = "testingSmtp || loadFailed"
class = "btn btn-secondary btn-sm"
>
< svg
v - if = "testingSmtp"
class = "h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
{ {
testingSmtp
? t ( "admin.settings.smtp.testing" )
: t ( "admin.settings.smtp.testConnection" )
} }
< / button >
2026-02-02 22:13:50 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "space-y-6 p-6" >
< div class = "grid grid-cols-1 gap-6 md:grid-cols-2" >
2026-03-02 19:37:40 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.smtp.host" ) } }
2026-03-02 19:37:40 +08:00
< / label >
< input
2026-04-21 17:35:12 +08:00
v - model = "form.smtp_host"
2026-03-02 19:37:40 +08:00
type = "text"
2026-04-21 17:35:12 +08:00
class = "input"
: placeholder = "t('admin.settings.smtp.hostPlaceholder')"
2026-03-02 19:37:40 +08:00
/ >
< / div >
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.smtp.port" ) } }
2026-03-02 19:37:40 +08:00
< / label >
< input
2026-04-21 17:35:12 +08:00
v - model . number = "form.smtp_port"
type = "number"
min = "1"
max = "65535"
class = "input"
: placeholder = "t('admin.settings.smtp.portPlaceholder')"
2026-03-02 19:37:40 +08:00
/ >
< / div >
2026-04-21 17:35:12 +08:00
< div >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.smtp.username" ) } }
2026-03-02 19:37:40 +08:00
< / label >
2026-04-21 17:35:12 +08:00
< input
v - model = "form.smtp_username"
type = "text"
class = "input"
: placeholder = "t('admin.settings.smtp.usernamePlaceholder')"
2026-03-03 06:20:10 +08:00
/ >
2026-03-02 19:37:40 +08:00
< / div >
2026-04-15 00:41:33 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.smtp.password" ) } }
< / label >
< input
v - model = "form.smtp_password"
type = "password"
class = "input"
autocomplete = "new-password"
autocapitalize = "off"
spellcheck = "false"
@ keydown = "smtpPasswordManuallyEdited = true"
@ paste = "smtpPasswordManuallyEdited = true"
: placeholder = "
form . smtp _password _configured
? t ( 'admin.settings.smtp.passwordConfiguredPlaceholder' )
: t ( 'admin.settings.smtp.passwordPlaceholder' )
"
/ >
< p class = "mt-1.5 text-xs text-gray-500 dark:text-gray-400" >
{ {
form . smtp _password _configured
? t ( "admin.settings.smtp.passwordConfiguredHint" )
: t ( "admin.settings.smtp.passwordHint" )
} }
< / p >
2026-04-15 00:41:33 +08:00
< / div >
2026-04-10 21:08:51 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.smtp.fromEmail" ) } }
< / label >
< input
v - model = "form.smtp_from_email"
type = "email"
class = "input"
: placeholder = "t('admin.settings.smtp.fromEmailPlaceholder')"
/ >
2026-04-10 21:08:51 +08:00
< / div >
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)
2026-04-12 00:02:26 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.smtp.fromName" ) } }
< / label >
< input
v - model = "form.smtp_from_name"
type = "text"
class = "input"
: placeholder = "t('admin.settings.smtp.fromNamePlaceholder')"
/ >
2026-04-10 21:08:51 +08:00
< / div >
< / div >
2026-04-20 17:39:57 +08:00
2026-04-21 17:35:12 +08:00
<!-- Use TLS Toggle -- >
< div
class = "flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
2026-04-10 21:08:51 +08:00
< div >
2026-04-21 17:35:12 +08:00
< label class = "font-medium text-gray-900 dark:text-white" > { {
t ( "admin.settings.smtp.useTls" )
} } < / label >
< p class = "text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.smtp.useTlsHint" ) } }
< / p >
2026-04-10 21:08:51 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.smtp_use_tls" / >
2026-04-10 21:08:51 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-04-10 21:08:51 +08:00
< / div >
2026-04-21 17:35:12 +08:00
<!-- Send Test Email - Only show when email verification is enabled -- >
< div v-if = "form.email_verify_enabled" class="card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h2 class = "text-lg font-semibold text-gray-900 dark:text-white" >
{ { t ( "admin.settings.testEmail.title" ) } }
< / h2 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.testEmail.description" ) } }
< / p >
< / div >
< div class = "p-6" >
< div class = "flex items-end gap-4" >
< div class = "flex-1" >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{ { t ( "admin.settings.testEmail.recipientEmail" ) } }
< / label >
< input
v - model = "testEmailAddress"
type = "email"
class = "input"
: placeholder = "
t ( 'admin.settings.testEmail.recipientEmailPlaceholder' )
"
/ >
< / div >
< button
type = "button"
@ click = "sendTestEmail"
: disabled = "
sendingTestEmail || ! testEmailAddress || loadFailed
"
class = "btn btn-secondary"
>
< svg
v - if = "sendingTestEmail"
class = "h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
>
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
< / svg >
{ {
sendingTestEmail
? t ( "admin.settings.testEmail.sending" )
: t ( "admin.settings.testEmail.sendTestEmail" )
} }
< / button >
2026-03-04 16:59:57 +08:00
< / div >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
<!-- Balance Low Notification -- >
< div class = "card" >
< div
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
< h3 class = "text-base font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.balanceNotify.title" ) } }
< / h3 >
2026-03-04 16:59:57 +08:00
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
2026-04-21 17:35:12 +08:00
{ { t ( "admin.settings.balanceNotify.description" ) } }
2026-03-04 16:59:57 +08:00
< / p >
< / div >
2026-04-21 17:35:12 +08:00
< div class = "px-6 py-6 space-y-4" >
< div class = "flex items-center justify-between" >
< label
class = "mb-0 block text-sm font-medium text-gray-700 dark:text-gray-300"
> { { t ( "admin.settings.balanceNotify.enabled" ) } } < / l a b e l
>
< Toggle v-model = "form.balance_low_notify_enabled" / >
2026-03-04 16:59:57 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div v-if = "form.balance_low_notify_enabled" >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
> { { t ( "admin.settings.balanceNotify.threshold" ) } } < / l a b e l
>
< div class = "relative" >
< span
class = "absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
> $ < / s p a n
>
< input
v - model . number = "form.balance_low_notify_threshold"
type = "number"
min = "0"
step = "0.01"
class = "input pl-7"
/ >
< / div >
< p class = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.balanceNotify.thresholdHint" ) } }
2026-03-04 16:59:57 +08:00
< / p >
< / div >
< div >
2026-04-21 17:35:12 +08:00
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
> { { t ( "admin.settings.balanceNotify.rechargeUrl" ) } } < / l a b e l
>
2026-03-04 16:59:57 +08:00
< input
2026-04-21 17:35:12 +08:00
v - model = "form.balance_low_notify_recharge_url"
type = "url"
2026-03-04 16:59:57 +08:00
class = "input"
2026-04-21 17:35:12 +08:00
: placeholder = "currentOrigin"
2026-03-04 16:59:57 +08:00
/ >
2026-04-21 17:35:12 +08:00
< p class = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.balanceNotify.rechargeUrlHint" ) } }
< / p >
2026-03-04 16:59:57 +08:00
< / div >
< / div >
2026-04-21 17:35:12 +08:00
< / div >
2026-03-04 16:59:57 +08:00
2026-04-21 17:35:12 +08:00
<!-- Account Quota Notification -- >
< div class = "card" >
2026-03-04 16:59:57 +08:00
< div
2026-04-21 17:35:12 +08:00
class = "border-b border-gray-100 px-6 py-4 dark:border-dark-700"
2026-03-04 16:59:57 +08:00
>
2026-04-21 17:35:12 +08:00
< h3 class = "text-base font-medium text-gray-900 dark:text-white" >
{ { t ( "admin.settings.quotaNotify.title" ) } }
< / h3 >
< p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.quotaNotify.description" ) } }
< / p >
2026-03-04 16:59:57 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div class = "px-6 py-6 space-y-4" >
< div class = "flex items-center justify-between" >
< label
class = "mb-0 block text-sm font-medium text-gray-700 dark:text-gray-300"
> { { t ( "admin.settings.quotaNotify.enabled" ) } } < / l a b e l
2025-12-25 08:41:36 -08:00
>
2026-04-21 17:35:12 +08:00
< Toggle v-model = "form.account_quota_notify_enabled" / >
2026-04-12 13:53:02 +08:00
< / div >
2026-04-21 17:35:12 +08:00
< div v-if = "form.account_quota_notify_enabled" >
< label
class = "mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
> { { t ( "admin.settings.quotaNotify.emails" ) } } < / l a b e l
>
< div class = "space-y-2" >
< div
v - for = " ( entry , index ) in form . account _quota _notify _emails ||
[ ] "
: key = "index"
class = "flex items-center gap-2"
>
< label
class = "relative inline-flex items-center cursor-pointer shrink-0"
>
< input
type = "checkbox"
: checked = "!entry.disabled"
@ change = "entry.disabled = !entry.disabled"
class = "sr-only peer"
/ >
< div
class = "w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-600 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:after:border-gray-500 peer-checked:bg-primary-600"
> < / div >
< / label >
< input
v - model = "entry.email"
type = "email"
class = "input flex-1"
: placeholder = "
t ( 'admin.settings.quotaNotify.emailPlaceholder' )
"
/ >
< button
@ click = "form.account_quota_notify_emails.splice(index, 1)"
class = "btn btn-secondary px-2"
type = "button"
>
< Icon name = "x" size = "xs" class = "h-4 w-4" / >
< / button >
< / div >
< button
@ click = "addQuotaNotifyEmail"
class = "btn btn-secondary btn-sm"
type = "button"
>
+ { { t ( "admin.settings.quotaNotify.addEmail" ) } }
2026-04-12 13:53:02 +08:00
< / button >
< / div >
2026-04-21 17:35:12 +08:00
< p class = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
{ { t ( "admin.settings.quotaNotify.emailsHint" ) } }
< / p >
2026-04-12 13:53:02 +08:00
< / div >
< / div >
< / div >
< / div >
2026-04-21 17:35:12 +08:00
<!-- / T a b : E m a i l - - >
2025-12-18 13:50:39 +08:00
2026-03-14 20:22:39 +08:00
<!-- Tab : Backup -- >
< div v-show = "activeTab === 'backup'" >
< BackupSettings / >
< / div >
2025-12-18 13:50:39 +08:00
<!-- Save Button -- >
2026-04-05 15:30:40 +08:00
< div v-show = "activeTab !== 'backup'" class="flex justify-end" >
2026-04-21 17:35:12 +08:00
< button
type = "submit"
: disabled = "saving || loadFailed"
class = "btn btn-primary"
>
< svg
v - if = "saving"
class = "h-4 w-4 animate-spin"
fill = "none"
viewBox = "0 0 24 24"
>
2025-12-25 08:41:36 -08:00
< circle
class = "opacity-25"
cx = "12"
cy = "12"
r = "10"
stroke = "currentColor"
stroke - width = "4"
> < / circle >
< path
class = "opacity-75"
fill = "currentColor"
d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
> < / path >
2025-12-18 13:50:39 +08:00
< / svg >
2026-04-21 17:35:12 +08:00
{ {
saving
? t ( "admin.settings.saving" )
: t ( "admin.settings.saveSettings" )
} }
2025-12-18 13:50:39 +08:00
< / button >
< / div >
< / form >
2026-04-10 21:08:51 +08:00
<!-- Provider dialogs placed outside the settings form to prevent form submission bubbling -- >
< PaymentProviderDialog
ref = "providerDialogRef"
: show = "showProviderDialog"
: saving = "providerSaving"
: editing = "editingProvider"
: all - key - options = "providerKeyOptions"
: enabled - key - options = "enabledProviderKeyOptions"
: all - payment - types = "allPaymentTypes"
: redirect - label = "t('admin.settings.payment.easypayRedirect')"
@ close = "showProviderDialog = false"
@ save = "handleSaveProvider"
/ >
2026-04-21 17:35:12 +08:00
< ConfirmDialog
: show = "showDeleteProviderDialog"
: title = "t('admin.settings.payment.deleteProvider')"
: message = "t('admin.settings.payment.deleteProviderConfirm')"
: confirm - text = "t('common.delete')"
danger
@ confirm = "handleDeleteProvider"
@ cancel = "showDeleteProviderDialog = false"
/ >
2025-12-18 13:50:39 +08:00
< / div >
< / AppLayout >
< / template >
< script setup lang = "ts" >
2026-04-21 17:35:12 +08:00
import { ref , reactive , computed , onMounted } from "vue" ;
import { useI18n } from "vue-i18n" ;
import { adminAPI } from "@/api" ;
2026-04-20 17:39:57 +08:00
import {
appendAuthSourceDefaultsToUpdateRequest ,
buildAuthSourceDefaultsState ,
2026-04-21 20:36:10 +08:00
defaultWeChatConnectScopesForMode ,
deriveWeChatConnectStoredMode ,
2026-04-20 17:39:57 +08:00
normalizeDefaultSubscriptionSettings ,
2026-04-21 20:36:10 +08:00
resolveWeChatConnectModeCapabilities ,
2026-04-21 17:35:12 +08:00
} from "@/api/admin/settings" ;
2026-03-02 03:41:50 +08:00
import type {
2026-04-20 17:39:57 +08:00
AuthSourceDefaultsState ,
AuthSourceType ,
2026-03-02 03:41:50 +08:00
SystemSettings ,
UpdateSettingsRequest ,
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)
2026-04-12 00:02:26 +08:00
DefaultSubscriptionSetting ,
2026-04-21 07:48:42 -07:00
WeChatConnectMode ,
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)
2026-04-12 00:02:26 +08:00
WebSearchEmulationConfig ,
WebSearchProviderConfig ,
2026-04-12 13:11:46 +08:00
WebSearchTestResult ,
2026-04-21 17:35:12 +08:00
} from "@/api/admin/settings" ;
import type { AdminGroup , Proxy , NotifyEmailEntry } from "@/types" ;
import type { ProviderInstance } from "@/types/payment" ;
import AppLayout from "@/components/layout/AppLayout.vue" ;
import Icon from "@/components/icons/Icon.vue" ;
import Select from "@/components/common/Select.vue" ;
import ConfirmDialog from "@/components/common/ConfirmDialog.vue" ;
import PaymentProviderList from "@/components/payment/PaymentProviderList.vue" ;
import PaymentProviderDialog from "@/components/payment/PaymentProviderDialog.vue" ;
import GroupBadge from "@/components/common/GroupBadge.vue" ;
import GroupOptionItem from "@/components/common/GroupOptionItem.vue" ;
import Toggle from "@/components/common/Toggle.vue" ;
import ProxySelector from "@/components/common/ProxySelector.vue" ;
import ImageUpload from "@/components/common/ImageUpload.vue" ;
import BackupSettings from "@/views/admin/BackupView.vue" ;
import { useClipboard } from "@/composables/useClipboard" ;
2026-04-22 00:35:34 +08:00
import { extractApiErrorMessage , extractI18nErrorMessage } from "@/utils/apiError" ;
2026-04-21 17:35:12 +08:00
import { useAppStore } from "@/stores" ;
import { useAdminSettingsStore } from "@/stores/adminSettings" ;
2026-04-21 23:17:45 +08:00
import { normalizeVisibleMethod } from "@/components/payment/paymentFlow" ;
2026-03-02 23:13:39 +08:00
import {
isRegistrationEmailSuffixDomainValid ,
normalizeRegistrationEmailSuffixDomain ,
normalizeRegistrationEmailSuffixDomains ,
2026-04-21 17:35:12 +08:00
parseRegistrationEmailSuffixWhitelistInput ,
} from "@/utils/registrationEmailPolicy" ;
2025-12-25 08:41:36 -08:00
2026-04-21 17:35:12 +08:00
const { t , locale } = useI18n ( ) ;
const appStore = useAppStore ( ) ;
const adminSettingsStore = useAdminSettingsStore ( ) ;
2026-03-04 16:59:57 +08:00
2026-04-21 23:26:45 +08:00
function localText ( zh : string , en : string ) : string {
return locale . value . startsWith ( "zh" ) ? zh : en ;
}
2026-04-22 14:57:47 +08:00
const paymentGuideHref = computed ( ( ) =>
locale . value . startsWith ( "zh" )
2026-04-22 18:48:40 +08:00
? "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md"
: "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT.md" ,
) ;
const paymentMethodsHref = computed ( ( ) =>
locale . value . startsWith ( "zh" )
? "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md#支持的支付方式"
: "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT.md#supported-payment-methods" ,
2026-04-22 14:57:47 +08:00
) ;
2026-04-21 17:35:12 +08:00
type SettingsTab =
| "general"
| "security"
| "users"
| "gateway"
| "payment"
| "email"
| "backup" ;
const activeTab = ref < SettingsTab > ( "general" ) ;
2026-03-04 16:59:57 +08:00
const settingsTabs = [
2026-04-21 17:35:12 +08:00
{ key : "general" as SettingsTab , icon : "home" as const } ,
{ key : "security" as SettingsTab , icon : "shield" as const } ,
{ key : "users" as SettingsTab , icon : "user" as const } ,
{ key : "gateway" as SettingsTab , icon : "server" as const } ,
{ key : "payment" as SettingsTab , icon : "creditCard" as const } ,
{ key : "email" as SettingsTab , icon : "mail" as const } ,
{ key : "backup" as SettingsTab , icon : "database" as const } ,
] ;
const { copyToClipboard } = useClipboard ( ) ;
const loading = ref ( true ) ;
const loadFailed = ref ( false ) ;
const saving = ref ( false ) ;
const testingSmtp = ref ( false ) ;
const sendingTestEmail = ref ( false ) ;
const smtpPasswordManuallyEdited = ref ( false ) ;
const testEmailAddress = ref ( "" ) ;
const registrationEmailSuffixWhitelistTags = ref < string [ ] > ( [ ] ) ;
const registrationEmailSuffixWhitelistDraft = ref ( "" ) ;
const tablePageSizeOptionsInput = ref ( "10, 20, 50, 100" ) ;
2025-12-18 13:50:39 +08:00
2025-12-20 15:11:43 +08:00
// Admin API Key 状态
2026-04-21 17:35:12 +08:00
const adminApiKeyLoading = ref ( true ) ;
const adminApiKeyExists = ref ( false ) ;
const adminApiKeyMasked = ref ( "" ) ;
const adminApiKeyOperating = ref ( false ) ;
const newAdminApiKey = ref ( "" ) ;
const subscriptionGroups = ref < AdminGroup [ ] > ( [ ] ) ;
2025-12-20 15:11:43 +08:00
2026-03-18 16:22:19 +08:00
// Overload Cooldown (529) 状态
2026-04-21 17:35:12 +08:00
const overloadCooldownLoading = ref ( true ) ;
const overloadCooldownSaving = ref ( false ) ;
2026-03-18 16:22:19 +08:00
const overloadCooldownForm = reactive ( {
enabled : true ,
2026-04-21 17:35:12 +08:00
cooldown _minutes : 10 ,
} ) ;
2026-03-18 16:22:19 +08:00
2026-01-11 21:54:52 -08:00
// Stream Timeout 状态
2026-04-21 17:35:12 +08:00
const streamTimeoutLoading = ref ( true ) ;
const streamTimeoutSaving = ref ( false ) ;
2026-01-11 21:54:52 -08:00
const streamTimeoutForm = reactive ( {
enabled : true ,
2026-04-21 17:35:12 +08:00
action : "temp_unsched" as "temp_unsched" | "error" | "none" ,
2026-01-11 21:54:52 -08:00
temp _unsched _minutes : 5 ,
threshold _count : 3 ,
2026-04-21 17:35:12 +08:00
threshold _window _minutes : 10 ,
} ) ;
2026-01-11 21:54:52 -08:00
2026-03-07 21:45:18 +08:00
// Rectifier 状态
2026-04-21 17:35:12 +08:00
const rectifierLoading = ref ( true ) ;
const rectifierSaving = ref ( false ) ;
2026-03-07 21:45:18 +08:00
const rectifierForm = reactive ( {
enabled : true ,
thinking _signature _enabled : true ,
2026-03-26 16:43:38 +08:00
thinking _budget _enabled : true ,
apikey _signature _enabled : false ,
2026-04-21 17:35:12 +08:00
apikey _signature _patterns : [ ] as string [ ] ,
} ) ;
2026-03-07 21:45:18 +08:00
2026-03-10 11:14:17 +08:00
// Beta Policy 状态
2026-04-21 17:35:12 +08:00
const betaPolicyLoading = ref ( true ) ;
const betaPolicySaving = ref ( false ) ;
2026-03-10 11:14:17 +08:00
const betaPolicyForm = reactive ( {
rules : [ ] as Array < {
2026-04-21 17:35:12 +08:00
beta _token : string ;
action : "pass" | "filter" | "block" ;
scope : "all" | "oauth" | "apikey" | "bedrock" ;
error _message ? : string ;
model _whitelist ? : string [ ] ;
fallback _action ? : "pass" | "filter" | "block" ;
fallback _error _message ? : string ;
} > ,
} ) ;
const tablePageSizeMin = 5 ;
const tablePageSizeMax = 1000 ;
const tablePageSizeDefault = 20 ;
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)
2026-04-12 00:02:26 +08:00
2026-03-02 03:41:50 +08:00
interface DefaultSubscriptionGroupOption {
2026-04-21 17:35:12 +08:00
value : number ;
label : string ;
description : string | null ;
platform : AdminGroup [ "platform" ] ;
subscriptionType : AdminGroup [ "subscription_type" ] ;
rate : number ;
[ key : string ] : unknown ;
2026-03-02 03:41:50 +08:00
}
2026-04-21 20:36:10 +08:00
type SettingsForm = Omit <
SystemSettings ,
2026-04-21 07:48:42 -07:00
| "wechat_connect_open_enabled"
| "wechat_connect_mp_enabled"
| "wechat_connect_mobile_enabled"
2026-04-21 20:36:10 +08:00
> & {
2026-04-21 17:35:12 +08:00
smtp _password : string ;
turnstile _secret _key : string ;
linuxdo _connect _client _secret : string ;
wechat _connect _app _secret : string ;
2026-04-21 07:48:42 -07:00
wechat _connect _open _app _secret : string ;
wechat _connect _mp _app _secret : string ;
wechat _connect _mobile _app _secret : string ;
2026-04-21 20:36:10 +08:00
wechat _connect _open _enabled : boolean ;
wechat _connect _mp _enabled : boolean ;
2026-04-21 07:48:42 -07:00
wechat _connect _mobile _enabled : boolean ;
2026-04-21 17:35:12 +08:00
oidc _connect _client _secret : string ;
force _email _on _third _party _signup : boolean ;
openai _advanced _scheduler _enabled : boolean ;
} ;
2026-01-02 17:40:57 +08:00
const form = reactive < SettingsForm > ( {
2025-12-18 13:50:39 +08:00
registration _enabled : true ,
email _verify _enabled : false ,
2026-03-02 23:13:39 +08:00
registration _email _suffix _whitelist : [ ] ,
2026-02-02 22:13:50 +08:00
promo _code _enabled : true ,
2026-01-29 16:29:59 +08:00
invitation _code _enabled : false ,
2026-02-02 22:13:50 +08:00
password _reset _enabled : false ,
totp _enabled : false ,
totp _encryption _key _configured : false ,
2025-12-18 13:50:39 +08:00
default _balance : 0 ,
default _concurrency : 1 ,
2026-03-02 03:41:50 +08:00
default _subscriptions : [ ] ,
2026-04-20 17:39:57 +08:00
force _email _on _third _party _signup : false ,
2026-04-23 03:33:52 +08:00
default _user _rpm _limit : 0 ,
2026-04-21 17:35:12 +08:00
site _name : "Sub2API" ,
site _logo : "" ,
site _subtitle : "Subscription to API Conversion Platform" ,
api _base _url : "" ,
contact _info : "" ,
doc _url : "" ,
home _content : "" ,
2026-03-12 02:42:57 +03:00
backend _mode _enabled : false ,
2026-02-02 22:13:50 +08:00
hide _ccs _import _button : false ,
2026-04-21 17:35:12 +08:00
payment _enabled : false ,
payment _min _amount : 1 ,
payment _max _amount : 10000 ,
payment _daily _limit : 50000 ,
payment _max _pending _orders : 3 ,
payment _order _timeout _minutes : 30 ,
payment _balance _disabled : false ,
payment _balance _recharge _multiplier : 1 ,
payment _recharge _fee _rate : 0 ,
payment _enabled _types : [ ] ,
payment _help _image _url : "" ,
payment _help _text : "" ,
payment _product _name _prefix : "" ,
payment _product _name _suffix : "" ,
payment _load _balance _strategy : "round-robin" ,
payment _cancel _rate _limit _enabled : false ,
payment _cancel _rate _limit _max : 10 ,
payment _cancel _rate _limit _window : 1 ,
payment _cancel _rate _limit _unit : "day" ,
payment _cancel _rate _limit _window _mode : "rolling" ,
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)
2026-04-12 00:02:26 +08:00
table _default _page _size : tablePageSizeDefault ,
table _page _size _options : [ 10 , 20 , 50 , 100 ] ,
2026-04-21 17:35:12 +08:00
custom _menu _items : [ ] as Array < {
id : string ;
label : string ;
icon _svg : string ;
url : string ;
visibility : "user" | "admin" ;
sort _order : number ;
} > ,
custom _endpoints : [ ] as Array < {
name : string ;
endpoint : string ;
description : string ;
} > ,
frontend _url : "" ,
smtp _host : "" ,
2025-12-18 13:50:39 +08:00
smtp _port : 587 ,
2026-04-21 17:35:12 +08:00
smtp _username : "" ,
smtp _password : "" ,
2026-01-02 17:40:57 +08:00
smtp _password _configured : false ,
2026-04-21 17:35:12 +08:00
smtp _from _email : "" ,
smtp _from _name : "" ,
2025-12-18 13:50:39 +08:00
smtp _use _tls : true ,
// Cloudflare Turnstile
turnstile _enabled : false ,
2026-04-21 17:35:12 +08:00
turnstile _site _key : "" ,
turnstile _secret _key : "" ,
2026-01-04 23:17:15 +08:00
turnstile _secret _key _configured : false ,
2026-01-12 09:14:32 +08:00
// LinuxDo Connect OAuth 登录
linuxdo _connect _enabled : false ,
2026-04-21 17:35:12 +08:00
linuxdo _connect _client _id : "" ,
linuxdo _connect _client _secret : "" ,
2026-01-12 09:14:32 +08:00
linuxdo _connect _client _secret _configured : false ,
2026-04-21 17:35:12 +08:00
linuxdo _connect _redirect _url : "" ,
wechat _connect _enabled : false ,
wechat _connect _app _id : "" ,
wechat _connect _app _secret : "" ,
wechat _connect _app _secret _configured : false ,
2026-04-21 07:48:42 -07:00
wechat _connect _open _app _id : "" ,
wechat _connect _open _app _secret : "" ,
wechat _connect _open _app _secret _configured : false ,
wechat _connect _mp _app _id : "" ,
wechat _connect _mp _app _secret : "" ,
wechat _connect _mp _app _secret _configured : false ,
wechat _connect _mobile _app _id : "" ,
wechat _connect _mobile _app _secret : "" ,
wechat _connect _mobile _app _secret _configured : false ,
2026-04-21 20:36:10 +08:00
wechat _connect _open _enabled : false ,
wechat _connect _mp _enabled : false ,
2026-04-21 07:48:42 -07:00
wechat _connect _mobile _enabled : false ,
2026-04-21 17:35:12 +08:00
wechat _connect _mode : "open" ,
wechat _connect _scopes : "snsapi_login" ,
wechat _connect _redirect _url : "" ,
wechat _connect _frontend _redirect _url : "/auth/wechat/callback" ,
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)
2026-04-12 00:02:26 +08:00
// Generic OIDC OAuth 登录
oidc _connect _enabled : false ,
2026-04-21 17:35:12 +08:00
oidc _connect _provider _name : "OIDC" ,
oidc _connect _client _id : "" ,
oidc _connect _client _secret : "" ,
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)
2026-04-12 00:02:26 +08:00
oidc _connect _client _secret _configured : false ,
2026-04-21 17:35:12 +08:00
oidc _connect _issuer _url : "" ,
oidc _connect _discovery _url : "" ,
oidc _connect _authorize _url : "" ,
oidc _connect _token _url : "" ,
oidc _connect _userinfo _url : "" ,
oidc _connect _jwks _url : "" ,
oidc _connect _scopes : "openid email profile" ,
oidc _connect _redirect _url : "" ,
oidc _connect _frontend _redirect _url : "/auth/oidc/callback" ,
oidc _connect _token _auth _method : "client_secret_post" ,
2026-04-22 11:17:32 +08:00
oidc _connect _use _pkce : false ,
oidc _connect _validate _id _token : false ,
2026-04-21 17:35:12 +08:00
oidc _connect _allowed _signing _algs : "RS256,ES256,PS256" ,
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)
2026-04-12 00:02:26 +08:00
oidc _connect _clock _skew _seconds : 120 ,
oidc _connect _require _email _verified : false ,
2026-04-21 17:35:12 +08:00
oidc _connect _userinfo _email _path : "" ,
oidc _connect _userinfo _id _path : "" ,
oidc _connect _userinfo _username _path : "" ,
2026-01-09 21:00:04 +08:00
// Model fallback
enable _model _fallback : false ,
2026-04-21 17:35:12 +08:00
fallback _model _anthropic : "claude-3-5-sonnet-20241022" ,
fallback _model _openai : "gpt-4o" ,
fallback _model _gemini : "gemini-2.5-pro" ,
fallback _model _antigravity : "gemini-2.5-pro" ,
2026-01-04 22:49:40 +08:00
// Identity patch (Claude -> Gemini)
enable _identity _patch : true ,
2026-04-21 17:35:12 +08:00
identity _patch _prompt : "" ,
2026-01-12 09:14:32 +08:00
// Ops monitoring (vNext)
ops _monitoring _enabled : true ,
ops _realtime _monitoring _enabled : true ,
2026-04-21 17:35:12 +08:00
ops _query _mode _default : "auto" ,
2026-03-01 15:35:46 +08:00
ops _metrics _interval _seconds : 60 ,
// Claude Code version check
2026-04-21 17:35:12 +08:00
min _claude _code _version : "" ,
max _claude _code _version : "" ,
2026-03-03 19:56:27 +08:00
// 分组隔离
2026-03-26 10:22:03 +08:00
allow _ungrouped _key _scheduling : false ,
2026-04-20 17:39:57 +08:00
openai _advanced _scheduler _enabled : false ,
2026-03-26 10:22:03 +08:00
// Gateway forwarding behavior
enable _fingerprint _unification : true ,
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)
2026-04-12 00:02:26 +08:00
enable _metadata _passthrough : false ,
2026-04-12 13:53:02 +08:00
enable _cch _signing : false ,
// Balance & quota notification
balance _low _notify _enabled : false ,
balance _low _notify _threshold : 0 ,
2026-04-21 17:35:12 +08:00
balance _low _notify _recharge _url : "" ,
2026-04-12 17:49:58 +08:00
account _quota _notify _enabled : false ,
2026-04-21 17:35:12 +08:00
account _quota _notify _emails : [ ] as NotifyEmailEntry [ ] ,
} ) ;
2026-01-12 09:14:32 +08:00
2026-04-21 17:35:12 +08:00
const authSourceDefaults = reactive < AuthSourceDefaultsState > (
buildAuthSourceDefaultsState ( { } ) ,
) ;
2026-04-20 17:39:57 +08:00
const authSourceDefaultsMeta = computed ( ( ) => [
{
2026-04-21 17:35:12 +08:00
source : "email" as AuthSourceType ,
2026-04-21 22:26:35 +08:00
title : t ( "admin.settings.authSourceDefaults.sources.email.title" ) ,
description : t ( "admin.settings.authSourceDefaults.sources.email.description" ) ,
2026-04-20 17:39:57 +08:00
} ,
{
2026-04-21 17:35:12 +08:00
source : "linuxdo" as AuthSourceType ,
2026-04-21 22:26:35 +08:00
title : t ( "admin.settings.authSourceDefaults.sources.linuxdo.title" ) ,
description : t ( "admin.settings.authSourceDefaults.sources.linuxdo.description" ) ,
2026-04-20 17:39:57 +08:00
} ,
{
2026-04-21 17:35:12 +08:00
source : "oidc" as AuthSourceType ,
2026-04-21 22:26:35 +08:00
title : t ( "admin.settings.authSourceDefaults.sources.oidc.title" ) ,
description : t ( "admin.settings.authSourceDefaults.sources.oidc.description" ) ,
2026-04-20 17:39:57 +08:00
} ,
{
2026-04-21 17:35:12 +08:00
source : "wechat" as AuthSourceType ,
2026-04-21 22:26:35 +08:00
title : t ( "admin.settings.authSourceDefaults.sources.wechat.title" ) ,
description : t ( "admin.settings.authSourceDefaults.sources.wechat.description" ) ,
2026-04-20 17:39:57 +08:00
} ,
2026-04-21 17:35:12 +08:00
] ) ;
2026-04-20 17:39:57 +08:00
2026-04-12 13:11:46 +08:00
// Proxies for web search emulation ProxySelector
2026-04-21 17:35:12 +08:00
const webSearchProxies = ref < Proxy [ ] > ( [ ] ) ;
2026-04-12 13:11:46 +08:00
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)
2026-04-12 00:02:26 +08:00
// Web Search Emulation config (loaded/saved separately)
2026-04-21 17:35:12 +08:00
const DEFAULT _WEB _SEARCH _QUOTA _LIMIT = 1000 ;
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)
2026-04-12 00:02:26 +08:00
const webSearchConfig = reactive < WebSearchEmulationConfig > ( {
enabled : false ,
providers : [ ] ,
2026-04-21 17:35:12 +08:00
} ) ;
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
const expandedProviders = reactive < Record < number , boolean > > ( { } ) ;
const apiKeyVisible = reactive < Record < number , boolean > > ( { } ) ;
const wsTestQuery = ref ( "" ) ;
const wsTestLoading = ref ( false ) ;
const wsTestResult = ref < WebSearchTestResult | null > ( null ) ;
const wsTestDialogOpen = ref ( false ) ;
2026-04-12 15:59:45 +08:00
function openTestDialog ( ) {
2026-04-21 17:35:12 +08:00
wsTestResult . value = null ;
wsTestDialogOpen . value = true ;
2026-04-12 15:59:45 +08:00
}
2026-04-12 13:11:46 +08:00
function toggleProviderExpand ( idx : number ) {
2026-04-21 17:35:12 +08:00
expandedProviders [ idx ] = ! expandedProviders [ idx ] ;
2026-04-12 13:11:46 +08:00
}
function removeWebSearchProvider ( idx : number ) {
2026-04-21 17:35:12 +08:00
webSearchConfig . providers . splice ( idx , 1 ) ;
2026-04-12 13:11:46 +08:00
// Re-index expandedProviders and apiKeyVisible after removal
2026-04-21 17:35:12 +08:00
const newExpanded : Record < number , boolean > = { } ;
const newVisible : Record < number , boolean > = { } ;
2026-04-12 13:11:46 +08:00
for ( let i = 0 ; i < webSearchConfig . providers . length ; i ++ ) {
2026-04-21 17:35:12 +08:00
const oldIdx = i >= idx ? i + 1 : i ;
newExpanded [ i ] = expandedProviders [ oldIdx ] ? ? false ;
newVisible [ i ] = apiKeyVisible [ oldIdx ] ? ? false ;
2026-04-12 13:11:46 +08:00
}
2026-04-21 17:35:12 +08:00
Object . keys ( expandedProviders ) . forEach (
( k ) => delete expandedProviders [ Number ( k ) ] ,
) ;
Object . keys ( apiKeyVisible ) . forEach ( ( k ) => delete apiKeyVisible [ Number ( k ) ] ) ;
Object . assign ( expandedProviders , newExpanded ) ;
Object . assign ( apiKeyVisible , newVisible ) ;
2026-04-12 13:11:46 +08:00
}
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)
2026-04-12 00:02:26 +08:00
function addWebSearchProvider ( ) {
2026-04-21 17:35:12 +08:00
const idx = webSearchConfig . providers . length ;
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)
2026-04-12 00:02:26 +08:00
webSearchConfig . providers . push ( {
2026-04-21 17:35:12 +08:00
type : "brave" ,
api _key : "" ,
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)
2026-04-12 00:02:26 +08:00
api _key _configured : false ,
quota _limit : DEFAULT _WEB _SEARCH _QUOTA _LIMIT ,
2026-04-12 13:11:46 +08:00
subscribed _at : null ,
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)
2026-04-12 00:02:26 +08:00
proxy _id : null ,
expires _at : null ,
2026-04-21 17:35:12 +08:00
} as WebSearchProviderConfig ) ;
expandedProviders [ idx ] = true ;
2026-04-12 13:11:46 +08:00
}
function formatSubscribedAt ( ts : number | null ) : string {
2026-04-21 17:35:12 +08:00
if ( ! ts ) return "" ;
2026-04-12 13:11:46 +08:00
// Use UTC to avoid timezone drift on repeated edits
2026-04-21 17:35:12 +08:00
const d = new Date ( ts * 1000 ) ;
const y = d . getUTCFullYear ( ) ;
const m = String ( d . getUTCMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
const day = String ( d . getUTCDate ( ) ) . padStart ( 2 , "0" ) ;
return ` ${ y } - ${ m } - ${ day } ` ;
2026-04-12 13:11:46 +08:00
}
function parseSubscribedAt ( dateStr : string ) : number | null {
2026-04-21 17:35:12 +08:00
if ( ! dateStr ) return null ;
2026-04-12 13:11:46 +08:00
// Parse as UTC to match formatSubscribedAt
2026-04-21 17:35:12 +08:00
return Math . floor ( new Date ( dateStr + "T00:00:00Z" ) . getTime ( ) / 1000 ) ;
2026-04-12 13:11:46 +08:00
}
function quotaPercentage ( provider : WebSearchProviderConfig ) : number {
2026-04-21 17:35:12 +08:00
if ( ! provider . quota _limit || provider . quota _limit <= 0 ) return 0 ;
return ( ( provider . quota _used ? ? 0 ) / provider . quota _limit ) * 100 ;
2026-04-12 13:11:46 +08:00
}
2026-04-14 08:03:27 +08:00
async function resetWebSearchUsage ( idx : number ) {
2026-04-21 17:35:12 +08:00
const provider = webSearchConfig . providers [ idx ] ;
if ( ! provider ) return ;
if ( ! confirm ( t ( "admin.settings.webSearchEmulation.resetUsageConfirm" ) ) )
return ;
2026-04-14 08:03:27 +08:00
try {
2026-04-21 17:35:12 +08:00
await adminAPI . settings . resetWebSearchUsage ( {
provider _type : provider . type ,
} ) ;
provider . quota _used = 0 ;
appStore . showSuccess (
t ( "admin.settings.webSearchEmulation.resetUsageSuccess" ) ,
) ;
2026-04-14 08:03:27 +08:00
} catch ( err : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( extractApiErrorMessage ( err , t ( "common.error" ) ) ) ;
2026-04-14 08:03:27 +08:00
}
}
2026-04-12 13:11:46 +08:00
async function copyApiKey ( idx : number ) {
2026-04-21 17:35:12 +08:00
const key = webSearchConfig . providers [ idx ] ? . api _key ;
2026-04-12 13:11:46 +08:00
if ( ! key ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
t ( "admin.settings.webSearchEmulation.apiKeyPlaceholder" ) ,
) ;
return ;
2026-04-12 13:11:46 +08:00
}
2026-04-12 14:43:12 +08:00
try {
2026-04-21 17:35:12 +08:00
await navigator . clipboard . writeText ( key ) ;
appStore . showSuccess ( t ( "admin.settings.webSearchEmulation.copied" ) ) ;
2026-04-12 14:43:12 +08:00
} catch {
2026-04-21 17:35:12 +08:00
appStore . showError ( t ( "common.error" ) ) ;
2026-04-12 14:43:12 +08:00
}
2026-04-12 13:11:46 +08:00
}
async function testWebSearchProvider ( ) {
2026-04-21 17:35:12 +08:00
wsTestLoading . value = true ;
wsTestResult . value = null ;
2026-04-12 13:11:46 +08:00
try {
2026-04-21 17:35:12 +08:00
const query =
wsTestQuery . value . trim ( ) ||
t ( "admin.settings.webSearchEmulation.testDefaultQuery" ) ;
wsTestResult . value = await adminAPI . settings . testWebSearchEmulation ( query ) ;
2026-04-12 13:11:46 +08:00
} catch ( err : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( extractApiErrorMessage ( err , t ( "common.error" ) ) ) ;
2026-04-12 13:11:46 +08:00
} finally {
2026-04-21 17:35:12 +08:00
wsTestLoading . value = false ;
2026-04-12 13:11:46 +08:00
}
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)
2026-04-12 00:02:26 +08:00
}
async function loadWebSearchConfig ( ) {
try {
2026-04-12 13:11:46 +08:00
const [ resp , proxiesResp ] = await Promise . all ( [
adminAPI . settings . getWebSearchEmulationConfig ( ) ,
adminAPI . proxies . list ( ) . catch ( ( ) => ( { items : [ ] as Proxy [ ] } ) ) ,
2026-04-21 17:35:12 +08:00
] ) ;
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)
2026-04-12 00:02:26 +08:00
if ( resp ) {
2026-04-21 17:35:12 +08:00
webSearchConfig . enabled = resp . enabled || false ;
webSearchConfig . providers = resp . providers || [ ] ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-21 17:35:12 +08:00
webSearchProxies . value = proxiesResp . items || [ ] ;
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)
2026-04-12 00:02:26 +08:00
} catch ( err : unknown ) {
// 404 is expected when config hasn't been created yet; show error for other failures
2026-04-21 17:35:12 +08:00
const status = ( err as { status ? : number } ) ? . status ;
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)
2026-04-12 00:02:26 +08:00
if ( status !== 404 && status !== undefined ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( extractApiErrorMessage ( err , t ( "common.error" ) ) ) ;
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)
2026-04-12 00:02:26 +08:00
}
}
}
async function saveWebSearchConfig ( ) : Promise < boolean > {
try {
2026-04-14 08:03:27 +08:00
for ( const p of webSearchConfig . providers ) {
2026-04-21 17:35:12 +08:00
const raw = p . quota _limit ;
2026-04-14 08:03:27 +08:00
if ( raw != null && Number ( raw ) !== 0 && Number ( raw ) < 1 ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
t ( "admin.settings.webSearchEmulation.quotaLimitMustBePositive" ) ,
) ;
return false ;
2026-04-14 08:03:27 +08:00
}
}
2026-04-21 17:35:12 +08:00
const providers = webSearchConfig . providers . map (
( p : WebSearchProviderConfig ) => ( {
... p ,
quota _limit : Number ( p . quota _limit ) > 0 ? Number ( p . quota _limit ) : null ,
} ) ,
) ;
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)
2026-04-12 00:02:26 +08:00
await adminAPI . settings . updateWebSearchEmulationConfig ( {
enabled : webSearchConfig . enabled ,
2026-04-14 07:22:22 +08:00
providers ,
2026-04-21 17:35:12 +08:00
} ) ;
return true ;
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)
2026-04-12 00:02:26 +08:00
} catch ( err : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( extractApiErrorMessage ( err , t ( "common.error" ) ) ) ;
return false ;
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)
2026-04-12 00:02:26 +08:00
}
}
2026-04-21 17:35:12 +08:00
const defaultSubscriptionGroupOptions = computed <
DefaultSubscriptionGroupOption [ ]
> ( ( ) =>
2026-03-02 03:41:50 +08:00
subscriptionGroups . value . map ( ( group ) => ( {
value : group . id ,
label : group . name ,
description : group . description ,
platform : group . platform ,
subscriptionType : group . subscription _type ,
2026-04-21 17:35:12 +08:00
rate : group . rate _multiplier ,
} ) ) ,
) ;
const registrationEmailSuffixWhitelistSeparatorKeys = new Set ( [
" " ,
"," ,
", " ,
"Enter" ,
"Tab" ,
] ) ;
2026-03-02 23:13:39 +08:00
function removeRegistrationEmailSuffixWhitelistTag ( suffix : string ) {
2026-04-21 17:35:12 +08:00
registrationEmailSuffixWhitelistTags . value =
registrationEmailSuffixWhitelistTags . value . filter (
( item ) => item !== suffix ,
) ;
2026-03-02 23:13:39 +08:00
}
function addRegistrationEmailSuffixWhitelistTag ( raw : string ) {
2026-04-21 17:35:12 +08:00
const suffix = normalizeRegistrationEmailSuffixDomain ( raw ) ;
2026-03-02 23:13:39 +08:00
if (
! isRegistrationEmailSuffixDomainValid ( suffix ) ||
registrationEmailSuffixWhitelistTags . value . includes ( suffix )
) {
2026-04-21 17:35:12 +08:00
return ;
2026-03-02 23:13:39 +08:00
}
registrationEmailSuffixWhitelistTags . value = [
... registrationEmailSuffixWhitelistTags . value ,
2026-04-21 17:35:12 +08:00
suffix ,
] ;
2026-03-02 23:13:39 +08:00
}
function commitRegistrationEmailSuffixWhitelistDraft ( ) {
if ( ! registrationEmailSuffixWhitelistDraft . value ) {
2026-04-21 17:35:12 +08:00
return ;
2026-03-02 23:13:39 +08:00
}
2026-04-21 17:35:12 +08:00
addRegistrationEmailSuffixWhitelistTag (
registrationEmailSuffixWhitelistDraft . value ,
) ;
registrationEmailSuffixWhitelistDraft . value = "" ;
2026-03-02 23:13:39 +08:00
}
function handleRegistrationEmailSuffixWhitelistDraftInput ( ) {
2026-04-21 17:35:12 +08:00
registrationEmailSuffixWhitelistDraft . value =
normalizeRegistrationEmailSuffixDomain (
registrationEmailSuffixWhitelistDraft . value ,
) ;
2026-03-02 23:13:39 +08:00
}
2026-04-21 17:35:12 +08:00
function handleRegistrationEmailSuffixWhitelistDraftKeydown (
event : KeyboardEvent ,
) {
2026-03-02 23:13:39 +08:00
if ( event . isComposing ) {
2026-04-21 17:35:12 +08:00
return ;
2026-03-02 23:13:39 +08:00
}
if ( registrationEmailSuffixWhitelistSeparatorKeys . has ( event . key ) ) {
2026-04-21 17:35:12 +08:00
event . preventDefault ( ) ;
commitRegistrationEmailSuffixWhitelistDraft ( ) ;
return ;
2026-03-02 23:13:39 +08:00
}
if (
2026-04-21 17:35:12 +08:00
event . key === "Backspace" &&
2026-03-02 23:13:39 +08:00
! registrationEmailSuffixWhitelistDraft . value &&
registrationEmailSuffixWhitelistTags . value . length > 0
) {
2026-04-21 17:35:12 +08:00
registrationEmailSuffixWhitelistTags . value . pop ( ) ;
2026-03-02 23:13:39 +08:00
}
}
function handleRegistrationEmailSuffixWhitelistPaste ( event : ClipboardEvent ) {
2026-04-21 17:35:12 +08:00
const text = event . clipboardData ? . getData ( "text" ) || "" ;
2026-03-02 23:13:39 +08:00
if ( ! text . trim ( ) ) {
2026-04-21 17:35:12 +08:00
return ;
2026-03-02 23:13:39 +08:00
}
2026-04-21 17:35:12 +08:00
event . preventDefault ( ) ;
const tokens = parseRegistrationEmailSuffixWhitelistInput ( text ) ;
2026-03-02 23:13:39 +08:00
for ( const token of tokens ) {
2026-04-21 17:35:12 +08:00
addRegistrationEmailSuffixWhitelistTag ( token ) ;
2026-03-02 23:13:39 +08:00
}
}
2026-04-12 13:53:02 +08:00
// Quota notify email helpers
const addQuotaNotifyEmail = ( ) => {
if ( ! form . account _quota _notify _emails ) {
2026-04-21 17:35:12 +08:00
form . account _quota _notify _emails = [ ] ;
2026-04-12 13:53:02 +08:00
}
2026-04-21 17:35:12 +08:00
form . account _quota _notify _emails . push ( {
email : "" ,
disabled : false ,
verified : true ,
} ) ;
} ;
2026-04-12 13:53:02 +08:00
2026-04-21 17:35:12 +08:00
const currentOrigin =
typeof window !== "undefined" ? window . location . origin : "" ;
2026-04-13 18:44:36 +08:00
2026-01-12 09:14:32 +08:00
// LinuxDo OAuth redirect URL suggestion
const linuxdoRedirectUrlSuggestion = computed ( ( ) => {
2026-04-21 17:35:12 +08:00
if ( typeof window === "undefined" ) return "" ;
2026-01-12 09:14:32 +08:00
const origin =
2026-04-21 17:35:12 +08:00
window . location . origin ||
` ${ window . location . protocol } // ${ window . location . host } ` ;
return ` ${ origin } /api/v1/auth/oauth/linuxdo/callback ` ;
} ) ;
2025-12-18 13:50:39 +08:00
2026-01-12 09:14:32 +08:00
async function setAndCopyLinuxdoRedirectUrl ( ) {
2026-04-21 17:35:12 +08:00
const url = linuxdoRedirectUrlSuggestion . value ;
if ( ! url ) return ;
form . linuxdo _connect _redirect _url = url ;
await copyToClipboard (
url ,
t ( "admin.settings.linuxdo.redirectUrlSetAndCopied" ) ,
) ;
}
const wechatRedirectUrlSuggestion = computed ( ( ) => {
if ( typeof window === "undefined" ) return "" ;
const origin =
window . location . origin ||
` ${ window . location . protocol } // ${ window . location . host } ` ;
return ` ${ origin } /api/v1/auth/oauth/wechat/callback ` ;
} ) ;
2026-04-21 07:48:42 -07:00
function syncWeChatConnectMode ( preferredMode ? : WeChatConnectMode ) {
if ( form . wechat _connect _mp _enabled && form . wechat _connect _mobile _enabled ) {
if ( preferredMode === "mobile" ) {
form . wechat _connect _mp _enabled = false ;
} else {
form . wechat _connect _mobile _enabled = false ;
}
}
2026-04-21 20:36:10 +08:00
const capabilities = resolveWeChatConnectModeCapabilities (
form . wechat _connect _open _enabled ,
form . wechat _connect _mp _enabled ,
2026-04-21 07:48:42 -07:00
form . wechat _connect _mobile _enabled ,
2026-04-21 20:36:10 +08:00
form . wechat _connect _mode ,
) ;
form . wechat _connect _open _enabled = capabilities . openEnabled ;
form . wechat _connect _mp _enabled = capabilities . mpEnabled ;
2026-04-21 07:48:42 -07:00
form . wechat _connect _mobile _enabled = capabilities . mobileEnabled ;
2026-04-21 20:36:10 +08:00
form . wechat _connect _mode = deriveWeChatConnectStoredMode (
capabilities . openEnabled ,
capabilities . mpEnabled ,
2026-04-21 07:48:42 -07:00
capabilities . mobileEnabled ,
2026-04-21 20:36:10 +08:00
form . wechat _connect _mode ,
) ;
form . wechat _connect _scopes = defaultWeChatConnectScopesForMode (
2026-04-21 17:35:12 +08:00
form . wechat _connect _mode ,
) ;
}
2026-01-12 09:14:32 +08:00
2026-04-21 07:48:42 -07:00
function handleWeChatOpenEnabledChange ( value : boolean ) {
form . wechat _connect _open _enabled = value ;
syncWeChatConnectMode ( value ? "open" : undefined ) ;
}
function handleWeChatMPEnabledChange ( value : boolean ) {
form . wechat _connect _mp _enabled = value ;
if ( value ) {
form . wechat _connect _mobile _enabled = false ;
}
syncWeChatConnectMode ( value ? "mp" : undefined ) ;
}
function handleWeChatMobileEnabledChange ( value : boolean ) {
form . wechat _connect _mobile _enabled = value ;
if ( value ) {
form . wechat _connect _mp _enabled = false ;
}
syncWeChatConnectMode ( value ? "mobile" : undefined ) ;
}
2026-04-21 17:35:12 +08:00
async function setAndCopyWeChatRedirectUrl ( ) {
const url = wechatRedirectUrlSuggestion . value ;
if ( ! url ) return ;
form . wechat _connect _redirect _url = url ;
await copyToClipboard (
url ,
2026-04-21 22:26:35 +08:00
t ( "admin.settings.wechatConnect.redirectUrlSetAndCopied" ) ,
2026-04-21 17:35:12 +08:00
) ;
2026-01-12 09:14:32 +08:00
}
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)
2026-04-12 00:02:26 +08:00
const oidcRedirectUrlSuggestion = computed ( ( ) => {
2026-04-21 17:35:12 +08:00
if ( typeof window === "undefined" ) return "" ;
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)
2026-04-12 00:02:26 +08:00
const origin =
2026-04-21 17:35:12 +08:00
window . location . origin ||
` ${ window . location . protocol } // ${ window . location . host } ` ;
return ` ${ origin } /api/v1/auth/oauth/oidc/callback ` ;
} ) ;
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)
2026-04-12 00:02:26 +08:00
async function setAndCopyOIDCRedirectUrl ( ) {
2026-04-21 17:35:12 +08:00
const url = oidcRedirectUrlSuggestion . value ;
if ( ! url ) return ;
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)
2026-04-12 00:02:26 +08:00
2026-04-21 17:35:12 +08:00
form . oidc _connect _redirect _url = url ;
await copyToClipboard ( url , t ( "admin.settings.oidc.redirectUrlSetAndCopied" ) ) ;
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)
2026-04-12 00:02:26 +08:00
}
2026-03-02 19:37:40 +08:00
// Custom menu item management
function addMenuItem ( ) {
form . custom _menu _items . push ( {
2026-04-21 17:35:12 +08:00
id : "" ,
label : "" ,
icon _svg : "" ,
url : "" ,
visibility : "user" ,
2026-03-02 19:37:40 +08:00
sort _order : form . custom _menu _items . length ,
2026-04-21 17:35:12 +08:00
} ) ;
2026-03-02 19:37:40 +08:00
}
function removeMenuItem ( index : number ) {
2026-04-21 17:35:12 +08:00
form . custom _menu _items . splice ( index , 1 ) ;
2026-03-02 19:37:40 +08:00
// Re-index sort_order
form . custom _menu _items . forEach ( ( item , i ) => {
2026-04-21 17:35:12 +08:00
item . sort _order = i ;
} ) ;
2026-03-02 19:37:40 +08:00
}
function moveMenuItem ( index : number , direction : - 1 | 1 ) {
2026-04-21 17:35:12 +08:00
const targetIndex = index + direction ;
if ( targetIndex < 0 || targetIndex >= form . custom _menu _items . length ) return ;
const items = form . custom _menu _items ;
const temp = items [ index ] ;
items [ index ] = items [ targetIndex ] ;
items [ targetIndex ] = temp ;
2026-03-02 19:37:40 +08:00
// Re-index sort_order
items . forEach ( ( item , i ) => {
2026-04-21 17:35:12 +08:00
item . sort _order = i ;
} ) ;
2026-03-02 19:37:40 +08:00
}
2026-03-24 10:13:28 +08:00
// Custom endpoint management
function addEndpoint ( ) {
2026-04-21 17:35:12 +08:00
form . custom _endpoints . push ( { name : "" , endpoint : "" , description : "" } ) ;
2026-03-24 10:13:28 +08:00
}
function removeEndpoint ( index : number ) {
2026-04-21 17:35:12 +08:00
form . custom _endpoints . splice ( index , 1 ) ;
2026-03-24 10:13:28 +08:00
}
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)
2026-04-12 00:02:26 +08:00
function formatTablePageSizeOptions ( options : number [ ] ) : string {
2026-04-21 17:35:12 +08:00
return options . join ( ", " ) ;
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)
2026-04-12 00:02:26 +08:00
}
function parseTablePageSizeOptionsInput ( raw : string ) : number [ ] | null {
const tokens = raw
2026-04-21 17:35:12 +08:00
. split ( "," )
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)
2026-04-12 00:02:26 +08:00
. map ( ( token ) => token . trim ( ) )
2026-04-21 17:35:12 +08:00
. filter ( ( token ) => token . length > 0 ) ;
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)
2026-04-12 00:02:26 +08:00
if ( tokens . length === 0 ) {
2026-04-21 17:35:12 +08:00
return null ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-21 17:35:12 +08:00
const parsed = tokens . map ( ( token ) => Number ( token ) ) ;
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)
2026-04-12 00:02:26 +08:00
if ( parsed . some ( ( value ) => ! Number . isInteger ( value ) ) ) {
2026-04-21 17:35:12 +08:00
return null ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-21 17:35:12 +08:00
const deduped = Array . from ( new Set ( parsed ) ) . sort ( ( a , b ) => a - b ) ;
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)
2026-04-12 00:02:26 +08:00
if (
2026-04-21 17:35:12 +08:00
deduped . some (
( value ) => value < tablePageSizeMin || value > tablePageSizeMax ,
)
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)
2026-04-12 00:02:26 +08:00
) {
2026-04-21 17:35:12 +08:00
return null ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-21 17:35:12 +08:00
return deduped ;
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)
2026-04-12 00:02:26 +08:00
}
2025-12-18 13:50:39 +08:00
async function loadSettings ( ) {
2026-04-21 17:35:12 +08:00
loading . value = true ;
loadFailed . value = false ;
2025-12-18 13:50:39 +08:00
try {
2026-04-21 17:35:12 +08:00
const settings = await adminAPI . settings . getSettings ( ) ;
settings . payment _load _balance _strategy =
settings . payment _load _balance _strategy || "round-robin" ;
2026-04-10 21:08:51 +08:00
// Only assign non-null values from backend (null means unconfigured, keep defaults)
for ( const [ key , value ] of Object . entries ( settings ) ) {
if ( value !== null && value !== undefined ) {
2026-04-21 17:35:12 +08:00
( form as Record < string , unknown > ) [ key ] = value ;
2026-04-10 21:08:51 +08:00
}
}
2026-04-21 17:35:12 +08:00
Object . assign ( authSourceDefaults , buildAuthSourceDefaultsState ( settings ) ) ;
form . backend _mode _enabled = settings . backend _mode _enabled ;
form . default _subscriptions = normalizeDefaultSubscriptionSettings (
settings . default _subscriptions ,
) ;
registrationEmailSuffixWhitelistTags . value =
normalizeRegistrationEmailSuffixDomains (
settings . registration _email _suffix _whitelist ,
) ;
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)
2026-04-12 00:02:26 +08:00
tablePageSizeOptionsInput . value = formatTablePageSizeOptions (
2026-04-21 17:35:12 +08:00
Array . isArray ( settings . table _page _size _options )
? settings . table _page _size _options
: [ 10 , 20 , 50 , 100 ] ,
) ;
registrationEmailSuffixWhitelistDraft . value = "" ;
form . smtp _password = "" ;
smtpPasswordManuallyEdited . value = false ;
form . turnstile _secret _key = "" ;
form . linuxdo _connect _client _secret = "" ;
form . wechat _connect _app _secret = "" ;
2026-04-21 07:48:42 -07:00
form . wechat _connect _open _app _secret = "" ;
form . wechat _connect _mp _app _secret = "" ;
form . wechat _connect _mobile _app _secret = "" ;
2026-04-21 20:36:10 +08:00
const wechatCapabilities = resolveWeChatConnectModeCapabilities (
settings . wechat _connect _open _enabled ,
settings . wechat _connect _mp _enabled ,
2026-04-21 07:48:42 -07:00
settings . wechat _connect _mobile _enabled ,
2026-04-21 20:36:10 +08:00
settings . wechat _connect _mode ,
) ;
form . wechat _connect _open _enabled = wechatCapabilities . openEnabled ;
form . wechat _connect _mp _enabled = wechatCapabilities . mpEnabled ;
2026-04-21 07:48:42 -07:00
form . wechat _connect _mobile _enabled = wechatCapabilities . mobileEnabled ;
2026-04-21 20:36:10 +08:00
form . wechat _connect _mode = deriveWeChatConnectStoredMode (
wechatCapabilities . openEnabled ,
wechatCapabilities . mpEnabled ,
2026-04-21 07:48:42 -07:00
wechatCapabilities . mobileEnabled ,
2026-04-21 17:35:12 +08:00
settings . wechat _connect _mode ,
) ;
2026-04-21 23:26:45 +08:00
const legacyWeChatAppID = String ( settings . wechat _connect _app _id || "" ) . trim ( ) ;
const legacyWeChatSecretConfigured = Boolean (
settings . wechat _connect _app _secret _configured ,
) ;
if ( ! form . wechat _connect _open _app _id && wechatCapabilities . openEnabled ) {
form . wechat _connect _open _app _id = legacyWeChatAppID ;
}
if ( ! form . wechat _connect _mp _app _id && wechatCapabilities . mpEnabled ) {
form . wechat _connect _mp _app _id = legacyWeChatAppID ;
}
if ( ! form . wechat _connect _mobile _app _id && wechatCapabilities . mobileEnabled ) {
form . wechat _connect _mobile _app _id = legacyWeChatAppID ;
}
if (
! form . wechat _connect _open _app _secret _configured &&
wechatCapabilities . openEnabled
) {
form . wechat _connect _open _app _secret _configured =
legacyWeChatSecretConfigured ;
}
if (
! form . wechat _connect _mp _app _secret _configured &&
wechatCapabilities . mpEnabled
) {
form . wechat _connect _mp _app _secret _configured = legacyWeChatSecretConfigured ;
}
if (
! form . wechat _connect _mobile _app _secret _configured &&
wechatCapabilities . mobileEnabled
) {
form . wechat _connect _mobile _app _secret _configured =
legacyWeChatSecretConfigured ;
}
2026-04-21 20:36:10 +08:00
form . wechat _connect _scopes = defaultWeChatConnectScopesForMode (
form . wechat _connect _mode ,
) ;
2026-04-21 17:35:12 +08:00
form . oidc _connect _client _secret = "" ;
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)
2026-04-12 00:02:26 +08:00
// Load web search emulation config separately
2026-04-21 17:35:12 +08:00
await loadWebSearchConfig ( ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
loadFailed . value = true ;
appStore . showError (
extractApiErrorMessage ( error , t ( "admin.settings.failedToLoad" ) ) ,
) ;
2025-12-18 13:50:39 +08:00
} finally {
2026-04-21 17:35:12 +08:00
loading . value = false ;
2025-12-18 13:50:39 +08:00
}
}
2026-03-02 03:41:50 +08:00
async function loadSubscriptionGroups ( ) {
try {
2026-04-21 17:35:12 +08:00
const groups = await adminAPI . groups . getAll ( ) ;
2026-03-02 03:41:50 +08:00
subscriptionGroups . value = groups . filter (
2026-04-21 17:35:12 +08:00
( group ) =>
group . subscription _type === "subscription" && group . status === "active" ,
) ;
2026-04-10 21:08:51 +08:00
} catch ( _error : unknown ) {
2026-04-21 17:35:12 +08:00
subscriptionGroups . value = [ ] ;
2026-03-02 03:41:50 +08:00
}
}
2026-04-20 17:39:57 +08:00
function findNextAvailableSubscriptionGroup (
2026-04-21 17:35:12 +08:00
existingGroupIDs : number [ ] ,
2026-04-20 17:39:57 +08:00
) : AdminGroup | undefined {
2026-04-21 17:35:12 +08:00
const existing = new Set ( existingGroupIDs ) ;
return subscriptionGroups . value . find ( ( group ) => ! existing . has ( group . id ) ) ;
2026-04-20 17:39:57 +08:00
}
2026-03-02 03:41:50 +08:00
function addDefaultSubscription ( ) {
2026-04-21 17:35:12 +08:00
if ( subscriptionGroups . value . length === 0 ) return ;
2026-04-20 17:39:57 +08:00
const candidate = findNextAvailableSubscriptionGroup (
2026-04-21 17:35:12 +08:00
form . default _subscriptions . map ( ( item ) => item . group _id ) ,
) ;
if ( ! candidate ) return ;
2026-03-02 03:41:50 +08:00
form . default _subscriptions . push ( {
group _id : candidate . id ,
2026-04-21 17:35:12 +08:00
validity _days : 30 ,
} ) ;
2026-03-02 03:41:50 +08:00
}
function removeDefaultSubscription ( index : number ) {
2026-04-21 17:35:12 +08:00
form . default _subscriptions . splice ( index , 1 ) ;
2026-03-02 03:41:50 +08:00
}
2026-04-20 17:39:57 +08:00
function addAuthSourceDefaultSubscription ( source : AuthSourceType ) {
2026-04-21 17:35:12 +08:00
if ( subscriptionGroups . value . length === 0 ) return ;
2026-04-20 17:39:57 +08:00
const candidate = findNextAvailableSubscriptionGroup (
2026-04-21 17:35:12 +08:00
authSourceDefaults [ source ] . subscriptions . map ( ( item ) => item . group _id ) ,
) ;
if ( ! candidate ) return ;
2026-04-20 17:39:57 +08:00
authSourceDefaults [ source ] . subscriptions . push ( {
group _id : candidate . id ,
2026-04-21 17:35:12 +08:00
validity _days : 30 ,
} ) ;
2026-04-20 17:39:57 +08:00
}
2026-04-21 17:35:12 +08:00
function removeAuthSourceDefaultSubscription (
source : AuthSourceType ,
index : number ,
) {
authSourceDefaults [ source ] . subscriptions . splice ( index , 1 ) ;
2026-04-20 17:39:57 +08:00
}
function findDuplicateDefaultSubscription (
2026-04-21 17:35:12 +08:00
subscriptions : DefaultSubscriptionSetting [ ] ,
2026-04-20 17:39:57 +08:00
) : DefaultSubscriptionSetting | undefined {
2026-04-21 17:35:12 +08:00
const seenGroupIDs = new Set < number > ( ) ;
2026-04-20 17:39:57 +08:00
return subscriptions . find ( ( item ) => {
if ( seenGroupIDs . has ( item . group _id ) ) {
2026-04-21 17:35:12 +08:00
return true ;
2026-04-20 17:39:57 +08:00
}
2026-04-21 17:35:12 +08:00
seenGroupIDs . add ( item . group _id ) ;
return false ;
} ) ;
2026-04-20 17:39:57 +08:00
}
2025-12-18 13:50:39 +08:00
async function saveSettings ( ) {
2026-04-21 17:35:12 +08:00
saving . value = true ;
2025-12-18 13:50:39 +08:00
try {
2026-04-21 17:35:12 +08:00
const normalizedTableDefaultPageSize = Math . floor (
Number ( form . table _default _page _size ) ,
) ;
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)
2026-04-12 00:02:26 +08:00
if (
! Number . isInteger ( normalizedTableDefaultPageSize ) ||
normalizedTableDefaultPageSize < tablePageSizeMin ||
normalizedTableDefaultPageSize > tablePageSizeMax
) {
appStore . showError (
2026-04-21 17:35:12 +08:00
t ( "admin.settings.site.tableDefaultPageSizeRangeError" , {
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)
2026-04-12 00:02:26 +08:00
min : tablePageSizeMin ,
2026-04-21 17:35:12 +08:00
max : tablePageSizeMax ,
} ) ,
) ;
return ;
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)
2026-04-12 00:02:26 +08:00
}
const normalizedTablePageSizeOptions = parseTablePageSizeOptionsInput (
2026-04-21 17:35:12 +08:00
tablePageSizeOptionsInput . value ,
) ;
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)
2026-04-12 00:02:26 +08:00
if ( ! normalizedTablePageSizeOptions ) {
appStore . showError (
2026-04-21 17:35:12 +08:00
t ( "admin.settings.site.tablePageSizeOptionsFormatError" , {
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)
2026-04-12 00:02:26 +08:00
min : tablePageSizeMin ,
2026-04-21 17:35:12 +08:00
max : tablePageSizeMax ,
} ) ,
) ;
return ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-21 17:35:12 +08:00
form . table _default _page _size = normalizedTableDefaultPageSize ;
form . table _page _size _options = normalizedTablePageSizeOptions ;
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)
2026-04-12 00:02:26 +08:00
2026-04-20 17:39:57 +08:00
const normalizedDefaultSubscriptions = normalizeDefaultSubscriptionSettings (
2026-04-21 17:35:12 +08:00
form . default _subscriptions ,
) ;
2026-04-20 17:39:57 +08:00
const duplicateDefaultSubscription = findDuplicateDefaultSubscription (
2026-04-21 17:35:12 +08:00
normalizedDefaultSubscriptions ,
) ;
2026-03-02 03:41:50 +08:00
if ( duplicateDefaultSubscription ) {
appStore . showError (
2026-04-21 17:35:12 +08:00
t ( "admin.settings.defaults.defaultSubscriptionsDuplicate" , {
groupId : duplicateDefaultSubscription . group _id ,
} ) ,
) ;
return ;
2026-03-02 03:41:50 +08:00
}
2026-04-20 17:39:57 +08:00
for ( const authSource of authSourceDefaultsMeta . value ) {
2026-04-21 17:35:12 +08:00
authSourceDefaults [ authSource . source ] . subscriptions =
normalizeDefaultSubscriptionSettings (
authSourceDefaults [ authSource . source ] . subscriptions ,
) ;
2026-04-20 17:39:57 +08:00
const duplicate = findDuplicateDefaultSubscription (
2026-04-21 17:35:12 +08:00
authSourceDefaults [ authSource . source ] . subscriptions ,
) ;
2026-04-20 17:39:57 +08:00
if ( duplicate ) {
appStore . showError (
2026-04-21 17:35:12 +08:00
` ${ authSource . title } : ${ t (
"admin.settings.defaults.defaultSubscriptionsDuplicate" ,
{
groupId : duplicate . group _id ,
} ,
) } ` ,
) ;
return ;
2026-04-20 17:39:57 +08:00
}
}
2026-04-21 07:48:42 -07:00
if ( form . wechat _connect _mp _enabled && form . wechat _connect _mobile _enabled ) {
appStore . showError (
localText (
"公众号和移动应用不能同时启用。" ,
"Official Account and Mobile App cannot be enabled at the same time." ,
) ,
) ;
return ;
}
2026-03-21 15:03:18 +08:00
// Validate URL fields — novalidate disables browser-native checks, so we validate here
const isValidHttpUrl = ( url : string ) : boolean => {
2026-04-21 17:35:12 +08:00
if ( ! url ) return true ;
2026-03-21 15:03:18 +08:00
try {
2026-04-21 17:35:12 +08:00
const u = new URL ( url ) ;
return u . protocol === "http:" || u . protocol === "https:" ;
2026-03-21 15:03:18 +08:00
} catch {
2026-04-21 17:35:12 +08:00
return false ;
2026-03-21 15:03:18 +08:00
}
2026-04-21 17:35:12 +08:00
} ;
2026-03-21 15:03:18 +08:00
// Optional URL fields: auto-clear invalid values so they don't cause backend 400 errors
2026-04-21 17:35:12 +08:00
if ( ! isValidHttpUrl ( form . frontend _url ) ) form . frontend _url = "" ;
if ( ! isValidHttpUrl ( form . doc _url ) ) form . doc _url = "" ;
2026-04-21 20:36:10 +08:00
syncWeChatConnectMode ( ) ;
const wechatStoredMode = deriveWeChatConnectStoredMode (
form . wechat _connect _open _enabled ,
form . wechat _connect _mp _enabled ,
2026-04-21 07:48:42 -07:00
form . wechat _connect _mobile _enabled ,
2026-04-21 20:36:10 +08:00
form . wechat _connect _mode ,
) ;
2026-03-21 15:03:18 +08:00
2026-01-02 17:40:57 +08:00
const payload : UpdateSettingsRequest = {
registration _enabled : form . registration _enabled ,
email _verify _enabled : form . email _verify _enabled ,
2026-04-21 17:35:12 +08:00
registration _email _suffix _whitelist :
registrationEmailSuffixWhitelistTags . value . map (
( suffix ) => ` @ ${ suffix } ` ,
) ,
2026-02-02 22:13:50 +08:00
promo _code _enabled : form . promo _code _enabled ,
2026-01-29 16:29:59 +08:00
invitation _code _enabled : form . invitation _code _enabled ,
2026-02-02 22:13:50 +08:00
password _reset _enabled : form . password _reset _enabled ,
totp _enabled : form . totp _enabled ,
2026-01-02 17:40:57 +08:00
default _balance : form . default _balance ,
default _concurrency : form . default _concurrency ,
2026-03-02 03:41:50 +08:00
default _subscriptions : normalizedDefaultSubscriptions ,
2026-04-20 17:39:57 +08:00
force _email _on _third _party _signup : form . force _email _on _third _party _signup ,
2026-04-23 03:33:52 +08:00
default _user _rpm _limit : form . default _user _rpm _limit ,
2026-01-02 17:40:57 +08:00
site _name : form . site _name ,
site _logo : form . site _logo ,
site _subtitle : form . site _subtitle ,
api _base _url : form . api _base _url ,
contact _info : form . contact _info ,
doc _url : form . doc _url ,
2026-01-10 18:37:44 +08:00
home _content : form . home _content ,
2026-03-12 02:42:57 +03:00
backend _mode _enabled : form . backend _mode _enabled ,
2026-02-02 22:13:50 +08:00
hide _ccs _import _button : form . hide _ccs _import _button ,
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)
2026-04-12 00:02:26 +08:00
table _default _page _size : form . table _default _page _size ,
table _page _size _options : form . table _page _size _options ,
2026-03-02 19:37:40 +08:00
custom _menu _items : form . custom _menu _items ,
2026-03-24 10:13:28 +08:00
custom _endpoints : form . custom _endpoints ,
2026-03-15 17:52:29 +08:00
frontend _url : form . frontend _url ,
2026-01-02 17:40:57 +08:00
smtp _host : form . smtp _host ,
smtp _port : form . smtp _port ,
smtp _username : form . smtp _username ,
smtp _password : form . smtp _password || undefined ,
smtp _from _email : form . smtp _from _email ,
smtp _from _name : form . smtp _from _name ,
smtp _use _tls : form . smtp _use _tls ,
turnstile _enabled : form . turnstile _enabled ,
turnstile _site _key : form . turnstile _site _key ,
2026-01-09 21:00:04 +08:00
turnstile _secret _key : form . turnstile _secret _key || undefined ,
2026-01-12 09:14:32 +08:00
linuxdo _connect _enabled : form . linuxdo _connect _enabled ,
linuxdo _connect _client _id : form . linuxdo _connect _client _id ,
2026-04-21 17:35:12 +08:00
linuxdo _connect _client _secret :
form . linuxdo _connect _client _secret || undefined ,
2026-01-12 09:14:32 +08:00
linuxdo _connect _redirect _url : form . linuxdo _connect _redirect _url ,
2026-04-21 17:35:12 +08:00
wechat _connect _enabled : form . wechat _connect _enabled ,
2026-04-21 07:48:42 -07:00
wechat _connect _app _id :
form . wechat _connect _open _app _id ||
form . wechat _connect _mp _app _id ||
form . wechat _connect _mobile _app _id ||
form . wechat _connect _app _id ,
2026-04-21 17:35:12 +08:00
wechat _connect _app _secret : form . wechat _connect _app _secret || undefined ,
2026-04-21 07:48:42 -07:00
wechat _connect _open _app _id : form . wechat _connect _open _app _id ,
wechat _connect _open _app _secret :
form . wechat _connect _open _app _secret || undefined ,
wechat _connect _mp _app _id : form . wechat _connect _mp _app _id ,
wechat _connect _mp _app _secret :
form . wechat _connect _mp _app _secret || undefined ,
wechat _connect _mobile _app _id : form . wechat _connect _mobile _app _id ,
wechat _connect _mobile _app _secret :
form . wechat _connect _mobile _app _secret || undefined ,
2026-04-21 20:36:10 +08:00
wechat _connect _open _enabled : form . wechat _connect _open _enabled ,
wechat _connect _mp _enabled : form . wechat _connect _mp _enabled ,
2026-04-21 07:48:42 -07:00
wechat _connect _mobile _enabled : form . wechat _connect _mobile _enabled ,
2026-04-21 20:36:10 +08:00
wechat _connect _mode : wechatStoredMode ,
2026-04-21 17:35:12 +08:00
wechat _connect _scopes :
2026-04-21 20:36:10 +08:00
defaultWeChatConnectScopesForMode ( wechatStoredMode ) ,
2026-04-21 17:35:12 +08:00
wechat _connect _redirect _url : form . wechat _connect _redirect _url ,
wechat _connect _frontend _redirect _url :
form . wechat _connect _frontend _redirect _url ,
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)
2026-04-12 00:02:26 +08:00
oidc _connect _enabled : form . oidc _connect _enabled ,
oidc _connect _provider _name : form . oidc _connect _provider _name ,
oidc _connect _client _id : form . oidc _connect _client _id ,
oidc _connect _client _secret : form . oidc _connect _client _secret || undefined ,
oidc _connect _issuer _url : form . oidc _connect _issuer _url ,
oidc _connect _discovery _url : form . oidc _connect _discovery _url ,
oidc _connect _authorize _url : form . oidc _connect _authorize _url ,
oidc _connect _token _url : form . oidc _connect _token _url ,
oidc _connect _userinfo _url : form . oidc _connect _userinfo _url ,
oidc _connect _jwks _url : form . oidc _connect _jwks _url ,
oidc _connect _scopes : form . oidc _connect _scopes ,
oidc _connect _redirect _url : form . oidc _connect _redirect _url ,
2026-04-21 17:35:12 +08:00
oidc _connect _frontend _redirect _url :
form . oidc _connect _frontend _redirect _url ,
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)
2026-04-12 00:02:26 +08:00
oidc _connect _token _auth _method : form . oidc _connect _token _auth _method ,
2026-04-22 11:17:32 +08:00
oidc _connect _use _pkce : form . oidc _connect _use _pkce ,
oidc _connect _validate _id _token : form . oidc _connect _validate _id _token ,
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)
2026-04-12 00:02:26 +08:00
oidc _connect _allowed _signing _algs : form . oidc _connect _allowed _signing _algs ,
oidc _connect _clock _skew _seconds : form . oidc _connect _clock _skew _seconds ,
2026-04-21 17:35:12 +08:00
oidc _connect _require _email _verified :
form . oidc _connect _require _email _verified ,
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)
2026-04-12 00:02:26 +08:00
oidc _connect _userinfo _email _path : form . oidc _connect _userinfo _email _path ,
oidc _connect _userinfo _id _path : form . oidc _connect _userinfo _id _path ,
2026-04-21 17:35:12 +08:00
oidc _connect _userinfo _username _path :
form . oidc _connect _userinfo _username _path ,
2026-01-09 21:00:04 +08:00
enable _model _fallback : form . enable _model _fallback ,
fallback _model _anthropic : form . fallback _model _anthropic ,
fallback _model _openai : form . fallback _model _openai ,
fallback _model _gemini : form . fallback _model _gemini ,
fallback _model _antigravity : form . fallback _model _antigravity ,
enable _identity _patch : form . enable _identity _patch ,
2026-03-01 15:35:46 +08:00
identity _patch _prompt : form . identity _patch _prompt ,
2026-03-03 19:56:27 +08:00
min _claude _code _version : form . min _claude _code _version ,
2026-03-20 09:10:01 +08:00
max _claude _code _version : form . max _claude _code _version ,
2026-03-26 10:22:03 +08:00
allow _ungrouped _key _scheduling : form . allow _ungrouped _key _scheduling ,
enable _fingerprint _unification : form . enable _fingerprint _unification ,
2026-04-08 16:11:19 +08:00
enable _metadata _passthrough : form . enable _metadata _passthrough ,
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)
2026-04-12 00:02:26 +08:00
enable _cch _signing : form . enable _cch _signing ,
2026-04-10 21:08:51 +08:00
// Payment configuration
payment _enabled : form . payment _enabled ,
payment _min _amount : Number ( form . payment _min _amount ) || 0 ,
payment _max _amount : Number ( form . payment _max _amount ) || 0 ,
payment _daily _limit : Number ( form . payment _daily _limit ) || 0 ,
payment _max _pending _orders : Number ( form . payment _max _pending _orders ) || 0 ,
2026-04-21 17:35:12 +08:00
payment _order _timeout _minutes :
Number ( form . payment _order _timeout _minutes ) || 0 ,
2026-04-10 21:08:51 +08:00
payment _balance _disabled : form . payment _balance _disabled ,
2026-04-21 17:35:12 +08:00
payment _balance _recharge _multiplier :
Number ( form . payment _balance _recharge _multiplier ) || 1 ,
2026-04-15 00:41:33 +08:00
payment _recharge _fee _rate : Number ( form . payment _recharge _fee _rate ) || 0 ,
2026-04-10 21:08:51 +08:00
payment _enabled _types : form . payment _enabled _types ,
payment _load _balance _strategy : form . payment _load _balance _strategy ,
payment _product _name _prefix : form . payment _product _name _prefix ,
payment _product _name _suffix : form . payment _product _name _suffix ,
payment _help _image _url : form . payment _help _image _url ,
payment _help _text : form . payment _help _text ,
payment _cancel _rate _limit _enabled : form . payment _cancel _rate _limit _enabled ,
2026-04-21 17:35:12 +08:00
payment _cancel _rate _limit _max :
Number ( form . payment _cancel _rate _limit _max ) || 10 ,
payment _cancel _rate _limit _window :
Number ( form . payment _cancel _rate _limit _window ) || 1 ,
2026-04-10 21:08:51 +08:00
payment _cancel _rate _limit _unit : form . payment _cancel _rate _limit _unit ,
2026-04-21 17:35:12 +08:00
payment _cancel _rate _limit _window _mode :
form . payment _cancel _rate _limit _window _mode ,
2026-04-20 17:39:57 +08:00
openai _advanced _scheduler _enabled : form . openai _advanced _scheduler _enabled ,
2026-04-12 13:53:02 +08:00
// Balance & quota notification
balance _low _notify _enabled : form . balance _low _notify _enabled ,
2026-04-21 17:35:12 +08:00
balance _low _notify _threshold :
Number ( form . balance _low _notify _threshold ) || 0 ,
balance _low _notify _recharge _url : ( form . balance _low _notify _recharge _url =
form . balance _low _notify _recharge _url || currentOrigin ) ,
2026-04-12 17:49:58 +08:00
account _quota _notify _enabled : form . account _quota _notify _enabled ,
2026-04-21 17:35:12 +08:00
account _quota _notify _emails : (
form . account _quota _notify _emails || [ ]
) . filter ( ( e ) => e . email . trim ( ) !== "" ) ,
} ;
2026-04-10 21:08:51 +08:00
2026-04-21 17:35:12 +08:00
appendAuthSourceDefaultsToUpdateRequest ( payload , authSourceDefaults ) ;
2026-04-20 17:39:57 +08:00
2026-04-21 17:35:12 +08:00
const updated = await adminAPI . settings . updateSettings ( payload ) ;
2026-04-10 21:08:51 +08:00
for ( const [ key , value ] of Object . entries ( updated ) ) {
if ( value !== null && value !== undefined ) {
2026-04-21 17:35:12 +08:00
( form as Record < string , unknown > ) [ key ] = value ;
2026-04-10 21:08:51 +08:00
}
}
2026-04-21 17:35:12 +08:00
Object . assign ( authSourceDefaults , buildAuthSourceDefaultsState ( updated ) ) ;
registrationEmailSuffixWhitelistTags . value =
normalizeRegistrationEmailSuffixDomains (
updated . registration _email _suffix _whitelist ,
) ;
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)
2026-04-12 00:02:26 +08:00
tablePageSizeOptionsInput . value = formatTablePageSizeOptions (
2026-04-21 17:35:12 +08:00
Array . isArray ( updated . table _page _size _options )
? updated . table _page _size _options
: [ 10 , 20 , 50 , 100 ] ,
) ;
registrationEmailSuffixWhitelistDraft . value = "" ;
form . smtp _password = "" ;
smtpPasswordManuallyEdited . value = false ;
form . turnstile _secret _key = "" ;
form . linuxdo _connect _client _secret = "" ;
form . wechat _connect _app _secret = "" ;
2026-04-21 07:48:42 -07:00
form . wechat _connect _open _app _secret = "" ;
form . wechat _connect _mp _app _secret = "" ;
form . wechat _connect _mobile _app _secret = "" ;
2026-04-21 20:36:10 +08:00
const updatedWechatCapabilities = resolveWeChatConnectModeCapabilities (
updated . wechat _connect _open _enabled ,
updated . wechat _connect _mp _enabled ,
2026-04-21 07:48:42 -07:00
updated . wechat _connect _mobile _enabled ,
2026-04-21 17:35:12 +08:00
updated . wechat _connect _mode ,
) ;
2026-04-21 20:36:10 +08:00
form . wechat _connect _open _enabled = updatedWechatCapabilities . openEnabled ;
form . wechat _connect _mp _enabled = updatedWechatCapabilities . mpEnabled ;
2026-04-21 07:48:42 -07:00
form . wechat _connect _mobile _enabled =
updatedWechatCapabilities . mobileEnabled ;
2026-04-21 20:36:10 +08:00
form . wechat _connect _mode = deriveWeChatConnectStoredMode (
updatedWechatCapabilities . openEnabled ,
updatedWechatCapabilities . mpEnabled ,
2026-04-21 07:48:42 -07:00
updatedWechatCapabilities . mobileEnabled ,
2026-04-21 20:36:10 +08:00
updated . wechat _connect _mode ,
) ;
form . wechat _connect _scopes = defaultWeChatConnectScopesForMode (
form . wechat _connect _mode ,
) ;
2026-04-21 17:35:12 +08:00
form . oidc _connect _client _secret = "" ;
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)
2026-04-12 00:02:26 +08:00
// Save web search emulation config separately (errors handled internally)
2026-04-21 17:35:12 +08:00
const wsOk = await saveWebSearchConfig ( ) ;
2026-03-04 10:44:28 +08:00
// Refresh cached settings so sidebar/header update immediately
2026-04-21 17:35:12 +08:00
await appStore . fetchPublicSettings ( true ) ;
await adminSettingsStore . fetch ( true ) ;
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)
2026-04-12 00:02:26 +08:00
if ( wsOk ) {
2026-04-21 17:35:12 +08:00
appStore . showSuccess ( t ( "admin.settings.settingsSaved" ) ) ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage ( error , t ( "admin.settings.failedToSave" ) ) ,
) ;
2025-12-18 13:50:39 +08:00
} finally {
2026-04-21 17:35:12 +08:00
saving . value = false ;
2025-12-18 13:50:39 +08:00
}
}
async function testSmtpConnection ( ) {
2026-04-21 17:35:12 +08:00
testingSmtp . value = true ;
2025-12-18 13:50:39 +08:00
try {
2026-04-21 17:35:12 +08:00
const smtpPasswordForTest = smtpPasswordManuallyEdited . value
? form . smtp _password
: "" ;
2025-12-18 13:50:39 +08:00
const result = await adminAPI . settings . testSmtpConnection ( {
smtp _host : form . smtp _host ,
smtp _port : form . smtp _port ,
smtp _username : form . smtp _username ,
2026-03-21 23:36:30 +08:00
smtp _password : smtpPasswordForTest ,
2026-04-21 17:35:12 +08:00
smtp _use _tls : form . smtp _use _tls ,
} ) ;
2025-12-18 13:50:39 +08:00
// API returns { message: "..." } on success, errors are thrown as exceptions
2026-04-21 17:35:12 +08:00
appStore . showSuccess (
result . message || t ( "admin.settings.smtpConnectionSuccess" ) ,
) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage ( error , t ( "admin.settings.failedToTestSmtp" ) ) ,
) ;
2025-12-18 13:50:39 +08:00
} finally {
2026-04-21 17:35:12 +08:00
testingSmtp . value = false ;
2025-12-18 13:50:39 +08:00
}
}
async function sendTestEmail ( ) {
if ( ! testEmailAddress . value ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( t ( "admin.settings.testEmail.enterRecipientHint" ) ) ;
return ;
2025-12-18 13:50:39 +08:00
}
2026-04-21 17:35:12 +08:00
sendingTestEmail . value = true ;
2025-12-18 13:50:39 +08:00
try {
2026-04-21 17:35:12 +08:00
const smtpPasswordForSend = smtpPasswordManuallyEdited . value
? form . smtp _password
: "" ;
2025-12-18 13:50:39 +08:00
const result = await adminAPI . settings . sendTestEmail ( {
email : testEmailAddress . value ,
smtp _host : form . smtp _host ,
smtp _port : form . smtp _port ,
smtp _username : form . smtp _username ,
2026-03-21 23:36:30 +08:00
smtp _password : smtpPasswordForSend ,
2025-12-18 13:50:39 +08:00
smtp _from _email : form . smtp _from _email ,
smtp _from _name : form . smtp _from _name ,
2026-04-21 17:35:12 +08:00
smtp _use _tls : form . smtp _use _tls ,
} ) ;
2025-12-18 13:50:39 +08:00
// API returns { message: "..." } on success, errors are thrown as exceptions
2026-04-21 17:35:12 +08:00
appStore . showSuccess ( result . message || t ( "admin.settings.testEmailSent" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage ( error , t ( "admin.settings.failedToSendTestEmail" ) ) ,
) ;
2025-12-18 13:50:39 +08:00
} finally {
2026-04-21 17:35:12 +08:00
sendingTestEmail . value = false ;
2025-12-18 13:50:39 +08:00
}
}
2025-12-20 15:11:43 +08:00
// Admin API Key 方法
async function loadAdminApiKey ( ) {
2026-04-21 17:35:12 +08:00
adminApiKeyLoading . value = true ;
2025-12-20 15:11:43 +08:00
try {
2026-04-21 17:35:12 +08:00
const status = await adminAPI . settings . getAdminApiKey ( ) ;
adminApiKeyExists . value = status . exists ;
adminApiKeyMasked . value = status . masked _key ;
2026-04-10 21:08:51 +08:00
} catch ( _error : unknown ) {
// Silent fail - admin API key status is non-critical
2025-12-20 15:11:43 +08:00
} finally {
2026-04-21 17:35:12 +08:00
adminApiKeyLoading . value = false ;
2025-12-20 15:11:43 +08:00
}
}
async function createAdminApiKey ( ) {
2026-04-21 17:35:12 +08:00
adminApiKeyOperating . value = true ;
2025-12-20 15:11:43 +08:00
try {
2026-04-21 17:35:12 +08:00
const result = await adminAPI . settings . regenerateAdminApiKey ( ) ;
newAdminApiKey . value = result . key ;
adminApiKeyExists . value = true ;
adminApiKeyMasked . value =
result . key . substring ( 0 , 10 ) + "..." + result . key . slice ( - 4 ) ;
appStore . showSuccess ( t ( "admin.settings.adminApiKey.keyGenerated" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( extractApiErrorMessage ( error , t ( "common.error" ) ) ) ;
2025-12-20 15:11:43 +08:00
} finally {
2026-04-21 17:35:12 +08:00
adminApiKeyOperating . value = false ;
2025-12-20 15:11:43 +08:00
}
}
async function regenerateAdminApiKey ( ) {
2026-04-21 17:35:12 +08:00
if ( ! confirm ( t ( "admin.settings.adminApiKey.regenerateConfirm" ) ) ) return ;
await createAdminApiKey ( ) ;
2025-12-20 15:11:43 +08:00
}
async function deleteAdminApiKey ( ) {
2026-04-21 17:35:12 +08:00
if ( ! confirm ( t ( "admin.settings.adminApiKey.deleteConfirm" ) ) ) return ;
adminApiKeyOperating . value = true ;
2025-12-20 15:11:43 +08:00
try {
2026-04-21 17:35:12 +08:00
await adminAPI . settings . deleteAdminApiKey ( ) ;
adminApiKeyExists . value = false ;
adminApiKeyMasked . value = "" ;
newAdminApiKey . value = "" ;
appStore . showSuccess ( t ( "admin.settings.adminApiKey.keyDeleted" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError ( extractApiErrorMessage ( error , t ( "common.error" ) ) ) ;
2025-12-20 15:11:43 +08:00
} finally {
2026-04-21 17:35:12 +08:00
adminApiKeyOperating . value = false ;
2025-12-20 15:11:43 +08:00
}
}
function copyNewKey ( ) {
2025-12-25 08:41:36 -08:00
navigator . clipboard
. writeText ( newAdminApiKey . value )
. then ( ( ) => {
2026-04-21 17:35:12 +08:00
appStore . showSuccess ( t ( "admin.settings.adminApiKey.keyCopied" ) ) ;
2025-12-25 08:41:36 -08:00
} )
. catch ( ( ) => {
2026-04-21 17:35:12 +08:00
appStore . showError ( t ( "common.copyFailed" ) ) ;
} ) ;
2025-12-20 15:11:43 +08:00
}
2026-03-18 16:22:19 +08:00
// Overload Cooldown 方法
async function loadOverloadCooldownSettings ( ) {
2026-04-21 17:35:12 +08:00
overloadCooldownLoading . value = true ;
2026-03-18 16:22:19 +08:00
try {
2026-04-21 17:35:12 +08:00
const settings = await adminAPI . settings . getOverloadCooldownSettings ( ) ;
Object . assign ( overloadCooldownForm , settings ) ;
2026-04-10 21:08:51 +08:00
} catch ( _error : unknown ) {
// Silent fail - settings will use defaults
2026-03-18 16:22:19 +08:00
} finally {
2026-04-21 17:35:12 +08:00
overloadCooldownLoading . value = false ;
2026-03-18 16:22:19 +08:00
}
}
async function saveOverloadCooldownSettings ( ) {
2026-04-21 17:35:12 +08:00
overloadCooldownSaving . value = true ;
2026-03-18 16:22:19 +08:00
try {
const updated = await adminAPI . settings . updateOverloadCooldownSettings ( {
enabled : overloadCooldownForm . enabled ,
2026-04-21 17:35:12 +08:00
cooldown _minutes : overloadCooldownForm . cooldown _minutes ,
} ) ;
Object . assign ( overloadCooldownForm , updated ) ;
appStore . showSuccess ( t ( "admin.settings.overloadCooldown.saved" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage (
error ,
t ( "admin.settings.overloadCooldown.saveFailed" ) ,
) ,
) ;
2026-03-18 16:22:19 +08:00
} finally {
2026-04-21 17:35:12 +08:00
overloadCooldownSaving . value = false ;
2026-03-18 16:22:19 +08:00
}
}
2026-01-11 21:54:52 -08:00
// Stream Timeout 方法
async function loadStreamTimeoutSettings ( ) {
2026-04-21 17:35:12 +08:00
streamTimeoutLoading . value = true ;
2026-01-11 21:54:52 -08:00
try {
2026-04-21 17:35:12 +08:00
const settings = await adminAPI . settings . getStreamTimeoutSettings ( ) ;
Object . assign ( streamTimeoutForm , settings ) ;
2026-04-10 21:08:51 +08:00
} catch ( _error : unknown ) {
// Silent fail - settings will use defaults
2026-01-11 21:54:52 -08:00
} finally {
2026-04-21 17:35:12 +08:00
streamTimeoutLoading . value = false ;
2026-01-11 21:54:52 -08:00
}
}
async function saveStreamTimeoutSettings ( ) {
2026-04-21 17:35:12 +08:00
streamTimeoutSaving . value = true ;
2026-01-11 21:54:52 -08:00
try {
const updated = await adminAPI . settings . updateStreamTimeoutSettings ( {
enabled : streamTimeoutForm . enabled ,
action : streamTimeoutForm . action ,
temp _unsched _minutes : streamTimeoutForm . temp _unsched _minutes ,
threshold _count : streamTimeoutForm . threshold _count ,
2026-04-21 17:35:12 +08:00
threshold _window _minutes : streamTimeoutForm . threshold _window _minutes ,
} ) ;
Object . assign ( streamTimeoutForm , updated ) ;
appStore . showSuccess ( t ( "admin.settings.streamTimeout.saved" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage (
error ,
t ( "admin.settings.streamTimeout.saveFailed" ) ,
) ,
) ;
2026-01-11 21:54:52 -08:00
} finally {
2026-04-21 17:35:12 +08:00
streamTimeoutSaving . value = false ;
2026-01-11 21:54:52 -08:00
}
}
2026-03-07 21:45:18 +08:00
// Rectifier 方法
async function loadRectifierSettings ( ) {
2026-04-21 17:35:12 +08:00
rectifierLoading . value = true ;
2026-03-07 21:45:18 +08:00
try {
2026-04-21 17:35:12 +08:00
const settings = await adminAPI . settings . getRectifierSettings ( ) ;
Object . assign ( rectifierForm , settings ) ;
2026-03-26 16:43:38 +08:00
// 确保 patterns 是数组(旧数据可能为 null)
if ( ! Array . isArray ( rectifierForm . apikey _signature _patterns ) ) {
2026-04-21 17:35:12 +08:00
rectifierForm . apikey _signature _patterns = [ ] ;
2026-03-26 16:43:38 +08:00
}
2026-04-10 21:08:51 +08:00
} catch ( _error : unknown ) {
// Silent fail - settings will use defaults
2026-03-07 21:45:18 +08:00
} finally {
2026-04-21 17:35:12 +08:00
rectifierLoading . value = false ;
2026-03-07 21:45:18 +08:00
}
}
async function saveRectifierSettings ( ) {
2026-04-21 17:35:12 +08:00
rectifierSaving . value = true ;
2026-03-07 21:45:18 +08:00
try {
const updated = await adminAPI . settings . updateRectifierSettings ( {
enabled : rectifierForm . enabled ,
thinking _signature _enabled : rectifierForm . thinking _signature _enabled ,
2026-03-26 16:43:38 +08:00
thinking _budget _enabled : rectifierForm . thinking _budget _enabled ,
apikey _signature _enabled : rectifierForm . apikey _signature _enabled ,
apikey _signature _patterns : rectifierForm . apikey _signature _patterns . filter (
2026-04-21 17:35:12 +08:00
( p ) => p . trim ( ) !== "" ,
) ,
} ) ;
Object . assign ( rectifierForm , updated ) ;
2026-03-26 16:43:38 +08:00
if ( ! Array . isArray ( rectifierForm . apikey _signature _patterns ) ) {
2026-04-21 17:35:12 +08:00
rectifierForm . apikey _signature _patterns = [ ] ;
2026-03-26 16:43:38 +08:00
}
2026-04-21 17:35:12 +08:00
appStore . showSuccess ( t ( "admin.settings.rectifier.saved" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage ( error , t ( "admin.settings.rectifier.saveFailed" ) ) ,
) ;
2026-03-07 21:45:18 +08:00
} finally {
2026-04-21 17:35:12 +08:00
rectifierSaving . value = false ;
2026-03-07 21:45:18 +08:00
}
}
2026-03-10 11:14:17 +08:00
const betaPolicyActionOptions = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{ value : "pass" , label : t ( "admin.settings.betaPolicy.actionPass" ) } ,
{ value : "filter" , label : t ( "admin.settings.betaPolicy.actionFilter" ) } ,
{ value : "block" , label : t ( "admin.settings.betaPolicy.actionBlock" ) } ,
] ) ;
2026-03-10 11:14:17 +08:00
const betaPolicyScopeOptions = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{ value : "all" , label : t ( "admin.settings.betaPolicy.scopeAll" ) } ,
{ value : "oauth" , label : t ( "admin.settings.betaPolicy.scopeOAuth" ) } ,
{ value : "apikey" , label : t ( "admin.settings.betaPolicy.scopeAPIKey" ) } ,
{ value : "bedrock" , label : t ( "admin.settings.betaPolicy.scopeBedrock" ) } ,
] ) ;
2026-03-10 11:14:17 +08:00
// Beta Policy 方法
const betaDisplayNames : Record < string , string > = {
2026-04-21 17:35:12 +08:00
"fast-mode-2026-02-01" : "Fast Mode" ,
"context-1m-2025-08-07" : "Context 1M" ,
} ;
2026-03-10 11:14:17 +08:00
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)
2026-04-12 00:02:26 +08:00
// 快捷预设:按 beta_token 定义预设方案
2026-04-21 17:35:12 +08:00
const betaPresets : Record <
string ,
Array < {
label : string ;
description : string ;
action : "pass" | "filter" | "block" ;
model _whitelist : string [ ] ;
fallback _action : "pass" | "filter" | "block" ;
} >
> = {
"context-1m-2025-08-07" : [
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)
2026-04-12 00:02:26 +08:00
{
2026-04-21 17:35:12 +08:00
label : t ( "admin.settings.betaPolicy.presetOpusOnly" ) ,
description : t ( "admin.settings.betaPolicy.presetOpusOnlyDesc" ) ,
action : "pass" ,
model _whitelist : [ "claude-opus-4-6" ] ,
fallback _action : "filter" ,
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)
2026-04-12 00:02:26 +08:00
} ,
] ,
2026-04-21 17:35:12 +08:00
} ;
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)
2026-04-12 00:02:26 +08:00
// 常用模型模式(具体 ID + 通配符示例)
2026-04-21 17:35:12 +08:00
const commonModelPatterns = [
"claude-opus-4-6" ,
"claude-sonnet-4-6" ,
"claude-opus-*" ,
"claude-sonnet-*" ,
] ;
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)
2026-04-12 00:02:26 +08:00
2026-03-10 11:14:17 +08:00
function getBetaDisplayName ( token : string ) : string {
2026-04-21 17:35:12 +08:00
return betaDisplayNames [ token ] || token ;
2026-03-10 11:14:17 +08:00
}
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)
2026-04-12 00:02:26 +08:00
function applyBetaPreset (
rule : ( typeof betaPolicyForm . rules ) [ number ] ,
2026-04-21 17:35:12 +08:00
preset : {
action : "pass" | "filter" | "block" ;
model _whitelist : string [ ] ;
fallback _action : "pass" | "filter" | "block" ;
} ,
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)
2026-04-12 00:02:26 +08:00
) {
2026-04-21 17:35:12 +08:00
rule . action = preset . action ;
rule . model _whitelist = [ ... preset . model _whitelist ] ;
rule . fallback _action = preset . fallback _action ;
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)
2026-04-12 00:02:26 +08:00
}
2026-04-21 17:35:12 +08:00
function addQuickPattern (
rule : ( typeof betaPolicyForm . rules ) [ number ] ,
pattern : string ,
) {
if ( ! rule . model _whitelist ) rule . model _whitelist = [ ] ;
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)
2026-04-12 00:02:26 +08:00
if ( ! rule . model _whitelist . includes ( pattern ) ) {
2026-04-21 17:35:12 +08:00
rule . model _whitelist . push ( pattern ) ;
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)
2026-04-12 00:02:26 +08:00
}
}
2026-03-10 11:14:17 +08:00
async function loadBetaPolicySettings ( ) {
2026-04-21 17:35:12 +08:00
betaPolicyLoading . value = true ;
2026-03-10 11:14:17 +08:00
try {
2026-04-21 17:35:12 +08:00
const settings = await adminAPI . settings . getBetaPolicySettings ( ) ;
betaPolicyForm . rules = settings . rules ;
2026-04-10 21:08:51 +08:00
} catch ( _error : unknown ) {
// Silent fail - settings will use defaults
2026-03-10 11:14:17 +08:00
} finally {
2026-04-21 17:35:12 +08:00
betaPolicyLoading . value = false ;
2026-03-10 11:14:17 +08:00
}
}
async function saveBetaPolicySettings ( ) {
2026-04-21 17:35:12 +08:00
betaPolicySaving . value = true ;
2026-03-10 11:14:17 +08:00
try {
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)
2026-04-12 00:02:26 +08:00
// Clean up empty patterns before saving
2026-04-21 17:35:12 +08:00
const cleanedRules = betaPolicyForm . rules . map ( ( rule ) => {
const whitelist = rule . model _whitelist ? . filter ( ( p ) => p . trim ( ) !== "" ) ;
const hasWhitelist = whitelist && whitelist . length > 0 ;
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)
2026-04-12 00:02:26 +08:00
return {
beta _token : rule . beta _token ,
action : rule . action ,
scope : rule . scope ,
error _message : rule . error _message ,
model _whitelist : hasWhitelist ? whitelist : undefined ,
2026-04-21 17:35:12 +08:00
fallback _action : hasWhitelist
? rule . fallback _action || "pass"
: undefined ,
fallback _error _message :
hasWhitelist && rule . fallback _action === "block"
? rule . fallback _error _message
: undefined ,
} ;
} ) ;
2026-03-10 11:14:17 +08:00
const updated = await adminAPI . settings . updateBetaPolicySettings ( {
2026-04-21 17:35:12 +08:00
rules : cleanedRules ,
} ) ;
betaPolicyForm . rules = updated . rules ;
appStore . showSuccess ( t ( "admin.settings.betaPolicy.saved" ) ) ;
2026-04-10 21:08:51 +08:00
} catch ( error : unknown ) {
2026-04-21 17:35:12 +08:00
appStore . showError (
extractApiErrorMessage ( error , t ( "admin.settings.betaPolicy.saveFailed" ) ) ,
) ;
2026-03-10 11:14:17 +08:00
} finally {
2026-04-21 17:35:12 +08:00
betaPolicySaving . value = false ;
2026-03-10 11:14:17 +08:00
}
}
2026-04-10 21:08:51 +08:00
// ==================== Provider Management ====================
const allPaymentTypes = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{ value : "easypay" , label : t ( "payment.methods.easypay" ) } ,
{ value : "alipay" , label : t ( "payment.methods.alipay" ) } ,
{ value : "wxpay" , label : t ( "payment.methods.wxpay" ) } ,
{ value : "stripe" , label : t ( "payment.methods.stripe" ) } ,
] ) ;
2026-04-10 21:08:51 +08:00
function isPaymentTypeEnabled ( type : string ) : boolean {
2026-04-21 17:35:12 +08:00
return form . payment _enabled _types . includes ( type ) ;
2026-04-10 21:08:51 +08:00
}
2026-04-21 17:35:12 +08:00
const hasAnyPaymentTypeEnabled = computed (
( ) => form . payment _enabled _types . length > 0 ,
) ;
2026-04-10 21:08:51 +08:00
function togglePaymentType ( type : string ) {
if ( form . payment _enabled _types . includes ( type ) ) {
2026-04-21 17:35:12 +08:00
form . payment _enabled _types = form . payment _enabled _types . filter (
( t ) => t !== type ,
) ;
2026-04-10 21:08:51 +08:00
// Disable all provider instances matching this type
2026-04-21 17:35:12 +08:00
disableProvidersByType ( type ) ;
2026-04-10 21:08:51 +08:00
} else {
2026-04-21 17:35:12 +08:00
form . payment _enabled _types = [ ... form . payment _enabled _types , type ] ;
2026-04-10 21:08:51 +08:00
}
}
async function disableProvidersByType ( type : string ) {
2026-04-21 17:35:12 +08:00
const matching = providers . value . filter (
( p ) => p . provider _key === type && p . enabled ,
) ;
2026-04-10 21:08:51 +08:00
for ( const p of matching ) {
try {
2026-04-21 17:35:12 +08:00
await adminAPI . payment . updateProvider ( p . id , { enabled : false } ) ;
p . enabled = false ;
2026-04-10 21:08:51 +08:00
} catch ( err : unknown ) {
2026-04-21 17:35:12 +08:00
slog ( "disable provider failed" , p . id , err ) ;
2026-04-10 21:08:51 +08:00
}
}
}
2026-04-21 17:35:12 +08:00
function slog ( ... args : unknown [ ] ) {
console . warn ( "[payment]" , ... args ) ;
}
2026-04-10 21:08:51 +08:00
2026-04-21 17:35:12 +08:00
const providersLoading = ref ( false ) ;
const providerSaving = ref ( false ) ;
const providers = ref < ProviderInstance [ ] > ( [ ] ) ;
const showProviderDialog = ref ( false ) ;
const showDeleteProviderDialog = ref ( false ) ;
const editingProvider = ref < ProviderInstance | null > ( null ) ;
const deletingProviderId = ref < number | null > ( null ) ;
const providerDialogRef = ref < InstanceType <
typeof PaymentProviderDialog
> | null > ( null ) ;
2026-04-10 21:08:51 +08:00
const providerKeyOptions = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{ value : "easypay" , label : t ( "admin.settings.payment.providerEasypay" ) } ,
{ value : "alipay" , label : t ( "admin.settings.payment.providerAlipay" ) } ,
{ value : "wxpay" , label : t ( "admin.settings.payment.providerWxpay" ) } ,
{ value : "stripe" , label : t ( "admin.settings.payment.providerStripe" ) } ,
] ) ;
2026-04-10 21:08:51 +08:00
const enabledProviderKeyOptions = computed ( ( ) => {
2026-04-21 17:35:12 +08:00
const enabled = form . payment _enabled _types ;
return providerKeyOptions . value . filter ( ( opt ) => enabled . includes ( opt . value ) ) ;
} ) ;
2026-04-10 21:08:51 +08:00
const loadBalanceOptions = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{
value : "round-robin" ,
label : t ( "admin.settings.payment.strategyRoundRobin" ) ,
} ,
{
value : "least-amount" ,
label : t ( "admin.settings.payment.strategyLeastAmount" ) ,
} ,
] ) ;
2026-04-10 21:08:51 +08:00
const cancelRateLimitUnitOptions = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{
value : "minute" ,
label : t ( "admin.settings.payment.cancelRateLimitUnitMinute" ) ,
} ,
{ value : "hour" , label : t ( "admin.settings.payment.cancelRateLimitUnitHour" ) } ,
{ value : "day" , label : t ( "admin.settings.payment.cancelRateLimitUnitDay" ) } ,
] ) ;
2026-04-10 21:08:51 +08:00
const cancelRateLimitModeOptions = computed ( ( ) => [
2026-04-21 17:35:12 +08:00
{
value : "rolling" ,
label : t ( "admin.settings.payment.cancelRateLimitWindowModeRolling" ) ,
} ,
{
value : "fixed" ,
label : t ( "admin.settings.payment.cancelRateLimitWindowModeFixed" ) ,
} ,
] ) ;
2026-04-10 21:08:51 +08:00
2026-04-21 23:17:45 +08:00
type ProviderEnablementCandidate = Pick <
ProviderInstance ,
"id" | "provider_key" | "supported_types" | "enabled" | "name"
> ;
function getProviderVisibleMethods (
provider : ProviderEnablementCandidate ,
) : Array < "alipay" | "wxpay" > {
if ( ! provider . enabled ) {
return [ ] ;
}
const supportedTypes = Array . isArray ( provider . supported _types )
? provider . supported _types
: [ ] ;
const methods = new Set < "alipay" | "wxpay" > ( ) ;
const addMethod = ( type : string ) => {
const method = normalizeVisibleMethod ( type ) ;
if ( method === "alipay" || method === "wxpay" ) {
methods . add ( method ) ;
}
} ;
if ( provider . provider _key === "alipay" ) {
if ( supportedTypes . length === 0 ) {
methods . add ( "alipay" ) ;
} else {
supportedTypes . forEach ( ( type ) => {
if ( normalizeVisibleMethod ( type ) === "alipay" ) {
methods . add ( "alipay" ) ;
}
} ) ;
}
} else if ( provider . provider _key === "wxpay" ) {
if ( supportedTypes . length === 0 ) {
methods . add ( "wxpay" ) ;
} else {
supportedTypes . forEach ( ( type ) => {
if ( normalizeVisibleMethod ( type ) === "wxpay" ) {
methods . add ( "wxpay" ) ;
}
} ) ;
}
} else if ( provider . provider _key === "easypay" ) {
supportedTypes . forEach ( addMethod ) ;
}
return Array . from ( methods ) ;
}
function findProviderEnablementConflict (
candidate : ProviderEnablementCandidate ,
) : { method : "alipay" | "wxpay" ; conflicting : ProviderInstance } | null {
const claimedMethods = getProviderVisibleMethods ( candidate ) ;
if ( claimedMethods . length === 0 ) {
return null ;
}
for ( const other of providers . value ) {
if ( other . id === candidate . id || ! other . enabled ) {
continue ;
}
const otherMethods = getProviderVisibleMethods ( other ) ;
const matchedMethod = claimedMethods . find ( ( method ) =>
otherMethods . includes ( method ) ,
) ;
if ( matchedMethod ) {
return {
method : matchedMethod ,
conflicting : other ,
} ;
}
}
return null ;
}
function showProviderEnablementConflict (
conflict : { method : "alipay" | "wxpay" ; conflicting : ProviderInstance } ,
) {
appStore . showError (
t ( "admin.settings.payment.enableConflict" , {
method : t ( ` payment.methods. ${ conflict . method } ` ) ,
provider : conflict . conflicting . name ,
} ) ,
) ;
}
2026-04-10 21:08:51 +08:00
async function loadProviders ( ) {
2026-04-21 17:35:12 +08:00
providersLoading . value = true ;
try {
const res = await adminAPI . payment . getProviders ( ) ;
providers . value = res . data || [ ] ;
} catch ( err : unknown ) {
2026-04-22 00:35:34 +08:00
appStore . showError ( extractI18nErrorMessage ( err , t , "payment.errors" , t ( "common.error" ) ) ) ;
2026-04-21 17:35:12 +08:00
} finally {
providersLoading . value = false ;
}
2026-04-10 21:08:51 +08:00
}
function openCreateProvider ( ) {
2026-04-21 17:35:12 +08:00
editingProvider . value = null ;
providerDialogRef . value ? . reset (
enabledProviderKeyOptions . value [ 0 ] ? . value || "easypay" ,
) ;
showProviderDialog . value = true ;
2026-04-10 21:08:51 +08:00
}
function openEditProvider ( provider : ProviderInstance ) {
2026-04-21 17:35:12 +08:00
editingProvider . value = provider ;
providerDialogRef . value ? . loadProvider ( provider ) ;
showProviderDialog . value = true ;
2026-04-10 21:08:51 +08:00
}
async function handleSaveProvider ( payload : Partial < ProviderInstance > ) {
2026-04-21 17:35:12 +08:00
providerSaving . value = true ;
2026-04-10 21:08:51 +08:00
try {
2026-04-21 23:17:45 +08:00
const candidate : ProviderEnablementCandidate = {
id : editingProvider . value ? . id ? ? 0 ,
provider _key :
payload . provider _key ? ? editingProvider . value ? . provider _key ? ? "" ,
supported _types :
payload . supported _types ? ? editingProvider . value ? . supported _types ? ? [ ] ,
enabled : payload . enabled ? ? editingProvider . value ? . enabled ? ? false ,
name : payload . name ? ? editingProvider . value ? . name ? ? "" ,
} ;
const conflict = findProviderEnablementConflict ( candidate ) ;
if ( conflict ) {
showProviderEnablementConflict ( conflict ) ;
return ;
}
2026-04-10 21:08:51 +08:00
if ( editingProvider . value ) {
2026-04-21 17:35:12 +08:00
await adminAPI . payment . updateProvider ( editingProvider . value . id , payload ) ;
2026-04-10 21:08:51 +08:00
} else {
2026-04-21 17:35:12 +08:00
await adminAPI . payment . createProvider ( payload ) ;
2026-04-10 21:08:51 +08:00
}
2026-04-21 17:35:12 +08:00
showProviderDialog . value = false ;
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)
2026-04-12 00:02:26 +08:00
// Reload full list (API returns decrypted/formatted data with correct sort order)
2026-04-21 17:35:12 +08:00
await loadProviders ( ) ;
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)
2026-04-12 00:02:26 +08:00
// Auto-save settings so provider changes take effect immediately
2026-04-21 17:35:12 +08:00
await saveSettings ( ) ;
2026-04-10 21:08:51 +08:00
} catch ( err : unknown ) {
2026-04-22 00:35:34 +08:00
appStore . showError ( extractI18nErrorMessage ( err , t , "payment.errors" , t ( "common.error" ) ) ) ;
2026-04-10 21:08:51 +08:00
} finally {
2026-04-21 17:35:12 +08:00
providerSaving . value = false ;
2026-04-10 21:08:51 +08:00
}
}
2026-04-21 17:35:12 +08:00
async function handleToggleField (
provider : ProviderInstance ,
field : "enabled" | "refund_enabled" | "allow_user_refund" ,
) {
let newValue : boolean ;
if ( field === "enabled" ) newValue = ! provider . enabled ;
else if ( field === "refund_enabled" ) newValue = ! provider . refund _enabled ;
else newValue = ! provider . allow _user _refund ;
2026-04-14 19:29:37 +08:00
2026-04-21 23:17:45 +08:00
if ( field === "enabled" && newValue ) {
const conflict = findProviderEnablementConflict ( {
id : provider . id ,
provider _key : provider . provider _key ,
supported _types : provider . supported _types ,
enabled : true ,
name : provider . name ,
} ) ;
if ( conflict ) {
showProviderEnablementConflict ( conflict ) ;
return ;
}
}
2026-04-21 17:35:12 +08:00
const payload : Record < string , boolean > = { [ field ] : newValue } ;
2026-04-14 19:29:37 +08:00
// Cascade: turning off refund_enabled also turns off allow_user_refund
2026-04-21 17:35:12 +08:00
if ( field === "refund_enabled" && ! newValue ) {
payload . allow _user _refund = false ;
2026-04-14 19:29:37 +08:00
}
2026-04-10 21:08:51 +08:00
try {
2026-04-21 17:35:12 +08:00
await adminAPI . payment . updateProvider ( provider . id , payload ) ;
2026-04-21 23:17:45 +08:00
await loadProviders ( ) ;
2026-04-21 17:35:12 +08:00
} catch ( err : unknown ) {
2026-04-22 00:35:34 +08:00
appStore . showError ( extractI18nErrorMessage ( err , t , "payment.errors" , t ( "common.error" ) ) ) ;
2026-04-21 17:35:12 +08:00
}
2026-04-10 21:08:51 +08:00
}
async function handleToggleType ( provider : ProviderInstance , type : string ) {
const updated = provider . supported _types . includes ( type )
2026-04-21 17:35:12 +08:00
? provider . supported _types . filter ( ( t ) => t !== type )
: [ ... provider . supported _types , type ] ;
2026-04-21 23:17:45 +08:00
const conflict = findProviderEnablementConflict ( {
id : provider . id ,
provider _key : provider . provider _key ,
supported _types : updated ,
enabled : provider . enabled ,
name : provider . name ,
} ) ;
if ( conflict ) {
showProviderEnablementConflict ( conflict ) ;
return ;
}
2026-04-10 21:08:51 +08:00
try {
2026-04-21 17:35:12 +08:00
await adminAPI . payment . updateProvider ( provider . id , {
supported _types : updated ,
} as any ) ;
2026-04-21 23:17:45 +08:00
await loadProviders ( ) ;
2026-04-21 17:35:12 +08:00
} catch ( err : unknown ) {
2026-04-22 00:35:34 +08:00
appStore . showError ( extractI18nErrorMessage ( err , t , "payment.errors" , t ( "common.error" ) ) ) ;
2026-04-21 17:35:12 +08:00
}
2026-04-10 21:08:51 +08:00
}
function confirmDeleteProvider ( provider : ProviderInstance ) {
2026-04-21 17:35:12 +08:00
deletingProviderId . value = provider . id ;
showDeleteProviderDialog . value = true ;
2026-04-10 21:08:51 +08:00
}
2026-04-21 17:35:12 +08:00
async function handleReorderProviders (
updates : { id : number ; sort _order : number } [ ] ,
) {
2026-04-10 21:08:51 +08:00
try {
await Promise . all (
2026-04-21 17:35:12 +08:00
updates . map ( ( u ) =>
adminAPI . payment . updateProvider ( u . id , {
sort _order : u . sort _order ,
} as Partial < ProviderInstance > ) ,
) ,
) ;
2026-04-21 23:17:45 +08:00
await loadProviders ( ) ;
2026-04-10 21:08:51 +08:00
} catch ( err : unknown ) {
2026-04-22 00:35:34 +08:00
appStore . showError ( extractI18nErrorMessage ( err , t , "payment.errors" , t ( "common.error" ) ) ) ;
2026-04-21 17:35:12 +08:00
loadProviders ( ) ;
2026-04-10 21:08:51 +08:00
}
}
async function handleDeleteProvider ( ) {
2026-04-21 17:35:12 +08:00
if ( ! deletingProviderId . value ) return ;
2026-04-10 21:08:51 +08:00
try {
2026-04-21 17:35:12 +08:00
await adminAPI . payment . deleteProvider ( deletingProviderId . value ) ;
appStore . showSuccess ( t ( "common.deleted" ) ) ;
showDeleteProviderDialog . value = false ;
loadProviders ( ) ;
} catch ( err : unknown ) {
2026-04-22 00:35:34 +08:00
appStore . showError ( extractI18nErrorMessage ( err , t , "payment.errors" , t ( "common.error" ) ) ) ;
2026-04-21 17:35:12 +08:00
}
2026-04-10 21:08:51 +08:00
}
2025-12-18 13:50:39 +08:00
onMounted ( ( ) => {
2026-04-21 17:35:12 +08:00
loadSettings ( ) ;
loadSubscriptionGroups ( ) ;
loadAdminApiKey ( ) ;
loadOverloadCooldownSettings ( ) ;
loadStreamTimeoutSettings ( ) ;
loadRectifierSettings ( ) ;
loadBetaPolicySettings ( ) ;
loadProviders ( ) ;
} ) ;
2025-12-18 13:50:39 +08:00
< / script >
2026-03-02 03:41:50 +08:00
< style scoped >
. default - sub - group - select : deep ( . select - trigger ) {
@ apply h - [ 42 px ] ;
}
. default - sub - delete - btn {
@ apply h - [ 42 px ] ;
}
2026-03-04 16:59:57 +08:00
/* ============ Settings Tab Navigation ============ */
2026-03-14 20:22:39 +08:00
/* Scroll container: thin scrollbar on PC, auto-hide on mobile */
. settings - tabs - scroll {
scrollbar - width : thin ;
scrollbar - color : transparent transparent ;
}
. settings - tabs - scroll : hover {
scrollbar - color : rgb ( 0 0 0 / 0.15 ) transparent ;
}
: root . dark . settings - tabs - scroll : hover {
scrollbar - color : rgb ( 255 255 255 / 0.2 ) transparent ;
}
. settings - tabs - scroll : : - webkit - scrollbar {
height : 3 px ;
}
. settings - tabs - scroll : : - webkit - scrollbar - track {
background : transparent ;
}
. settings - tabs - scroll : : - webkit - scrollbar - thumb {
background : transparent ;
border - radius : 3 px ;
}
. settings - tabs - scroll : hover : : - webkit - scrollbar - thumb {
background : rgb ( 0 0 0 / 0.15 ) ;
}
: root . dark . settings - tabs - scroll : hover : : - webkit - scrollbar - thumb {
background : rgb ( 255 255 255 / 0.2 ) ;
}
2026-03-04 16:59:57 +08:00
. settings - tabs {
2026-03-14 20:22:39 +08:00
@ apply inline - flex min - w - full gap - 0.5 rounded - 2 xl
border border - gray - 100 bg - white / 80 p - 1 backdrop - blur - sm
2026-03-04 16:59:57 +08:00
dark : border - dark - 700 / 50 dark : bg - dark - 800 / 80 ;
2026-04-21 17:35:12 +08:00
box - shadow :
0 1 px 3 px rgb ( 0 0 0 / 0.04 ) ,
0 1 px 2 px rgb ( 0 0 0 / 0.02 ) ;
2026-03-04 16:59:57 +08:00
}
@ media ( min - width : 640 px ) {
. settings - tabs {
@ apply flex ;
}
}
. settings - tab {
2026-03-14 20:22:39 +08:00
@ apply relative flex flex - 1 items - center justify - center gap - 1.5
whitespace - nowrap rounded - xl px - 2.5 py - 2
2026-03-04 16:59:57 +08:00
text - sm font - medium
text - gray - 500 dark : text - dark - 400
transition - all duration - 200 ease - out ;
}
. settings - tab : hover : not ( . settings - tab - active ) {
@ apply text - gray - 700 dark : text - gray - 300 ;
background : rgb ( 0 0 0 / 0.03 ) ;
}
: root . dark . settings - tab : hover : not ( . settings - tab - active ) {
background : rgb ( 255 255 255 / 0.04 ) ;
}
. settings - tab - active {
@ apply text - primary - 600 dark : text - primary - 400 ;
2026-04-21 17:35:12 +08:00
background : linear - gradient (
135 deg ,
rgba ( 20 , 184 , 166 , 0.08 ) ,
rgba ( 20 , 184 , 166 , 0.03 )
) ;
2026-03-04 16:59:57 +08:00
box - shadow : 0 1 px 2 px rgba ( 20 , 184 , 166 , 0.1 ) ;
}
: root . dark . settings - tab - active {
2026-04-21 17:35:12 +08:00
background : linear - gradient (
135 deg ,
rgba ( 45 , 212 , 191 , 0.12 ) ,
rgba ( 45 , 212 , 191 , 0.05 )
) ;
2026-03-04 16:59:57 +08:00
box - shadow : 0 1 px 3 px rgb ( 0 0 0 / 0.25 ) ;
}
. settings - tab - icon {
2026-03-14 20:22:39 +08:00
@ apply flex h - 6 w - 6 items - center justify - center rounded - lg
2026-03-04 16:59:57 +08:00
transition - all duration - 200 ;
}
. settings - tab - active . settings - tab - icon {
@ apply bg - primary - 500 / 15 text - primary - 600
dark : bg - primary - 400 / 15 dark : text - primary - 400 ;
}
2026-03-02 03:41:50 +08:00
< / style >