diff --git a/web/backend/api/config.go b/web/backend/api/config.go index fb2f01f25..8d3381768 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -191,6 +191,9 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { } type webSearchAPIKeyPatch struct { + braveAPIKey *string + tavilyAPIKey *string + perplexityAPIKey *string glmSearchAPIKey *string baiduSearchAPIKey *string } @@ -199,6 +202,15 @@ func parseWebSearchAPIKeyPatch(body []byte) webSearchAPIKeyPatch { var raw struct { Tools *struct { Web *struct { + Brave *struct { + APIKey *string `json:"api_key"` + } `json:"brave"` + Tavily *struct { + APIKey *string `json:"api_key"` + } `json:"tavily"` + Perplexity *struct { + APIKey *string `json:"api_key"` + } `json:"perplexity"` GLMSearch *struct { APIKey *string `json:"api_key"` } `json:"glm_search"` @@ -214,6 +226,15 @@ func parseWebSearchAPIKeyPatch(body []byte) webSearchAPIKeyPatch { patch := webSearchAPIKeyPatch{} if raw.Tools != nil && raw.Tools.Web != nil { + if raw.Tools.Web.Brave != nil && raw.Tools.Web.Brave.APIKey != nil { + patch.braveAPIKey = raw.Tools.Web.Brave.APIKey + } + if raw.Tools.Web.Tavily != nil && raw.Tools.Web.Tavily.APIKey != nil { + patch.tavilyAPIKey = raw.Tools.Web.Tavily.APIKey + } + if raw.Tools.Web.Perplexity != nil && raw.Tools.Web.Perplexity.APIKey != nil { + patch.perplexityAPIKey = raw.Tools.Web.Perplexity.APIKey + } if raw.Tools.Web.GLMSearch != nil && raw.Tools.Web.GLMSearch.APIKey != nil { patch.glmSearchAPIKey = raw.Tools.Web.GLMSearch.APIKey } @@ -225,6 +246,15 @@ func parseWebSearchAPIKeyPatch(body []byte) webSearchAPIKeyPatch { } func applyWebSearchAPIKeyPatch(cfg *config.Config, patch webSearchAPIKeyPatch) { + if patch.braveAPIKey != nil { + cfg.Tools.Web.Brave.SetAPIKey(strings.TrimSpace(*patch.braveAPIKey)) + } + if patch.tavilyAPIKey != nil { + cfg.Tools.Web.Tavily.SetAPIKey(strings.TrimSpace(*patch.tavilyAPIKey)) + } + if patch.perplexityAPIKey != nil { + cfg.Tools.Web.Perplexity.SetAPIKey(strings.TrimSpace(*patch.perplexityAPIKey)) + } if patch.glmSearchAPIKey != nil { cfg.Tools.Web.GLMSearch.SetAPIKey(strings.TrimSpace(*patch.glmSearchAPIKey)) } diff --git a/web/backend/api/config_test.go b/web/backend/api/config_test.go index ad1ab5a67..0e5f3f489 100644 --- a/web/backend/api/config_test.go +++ b/web/backend/api/config_test.go @@ -160,32 +160,61 @@ func runConfigRequest(t *testing.T, configPath, method, body string) { } } -func assertWebSearchAPIKeys(t *testing.T, configPath, wantGLMAPIKey, wantBaiduAPIKey string) { +type webSearchAPIKeys struct { + brave string + tavily string + perplexity string + glm string + baidu string +} + +func assertWebSearchAPIKeys(t *testing.T, configPath string, want webSearchAPIKeys) { t.Helper() cfg, err := config.LoadConfig(configPath) if err != nil { t.Fatalf("LoadConfig() error = %v", err) } - if got := cfg.Tools.Web.GLMSearch.APIKey(); got != wantGLMAPIKey { - t.Fatalf("tools.web.glm_search.api_key = %q, want %q", got, wantGLMAPIKey) + if got := cfg.Tools.Web.Brave.APIKey(); got != want.brave { + t.Fatalf("tools.web.brave.api_key = %q, want %q", got, want.brave) } - if got := cfg.Tools.Web.BaiduSearch.APIKey(); got != wantBaiduAPIKey { - t.Fatalf("tools.web.baidu_search.api_key = %q, want %q", got, wantBaiduAPIKey) + if got := cfg.Tools.Web.Tavily.APIKey(); got != want.tavily { + t.Fatalf("tools.web.tavily.api_key = %q, want %q", got, want.tavily) + } + if got := cfg.Tools.Web.Perplexity.APIKey(); got != want.perplexity { + t.Fatalf("tools.web.perplexity.api_key = %q, want %q", got, want.perplexity) + } + if got := cfg.Tools.Web.GLMSearch.APIKey(); got != want.glm { + t.Fatalf("tools.web.glm_search.api_key = %q, want %q", got, want.glm) + } + if got := cfg.Tools.Web.BaiduSearch.APIKey(); got != want.baidu { + t.Fatalf("tools.web.baidu_search.api_key = %q, want %q", got, want.baidu) } } func TestHandlePatchConfig_PreservesWebSearchAPIKeysWhenOmitted(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() - seedWebSearchAPIKeys(t, configPath, "glm-existing-key", "baidu-existing-key") + seedWebSearchAPIKeys(t, configPath, webSearchAPIKeys{ + brave: "brave-existing-key", + tavily: "tavily-existing-key", + perplexity: "perplexity-existing-key", + glm: "glm-existing-key", + baidu: "baidu-existing-key", + }) runConfigRequest(t, configPath, http.MethodPatch, `{ "gateway": { "log_level": "info" } }`) - assertWebSearchAPIKeys(t, configPath, "glm-existing-key", "baidu-existing-key") + assertWebSearchAPIKeys(t, configPath, webSearchAPIKeys{ + brave: "brave-existing-key", + tavily: "tavily-existing-key", + perplexity: "perplexity-existing-key", + glm: "glm-existing-key", + baidu: "baidu-existing-key", + }) } func TestHandlePatchConfig_UpdatesWebSearchAPIKeys(t *testing.T) { @@ -195,18 +224,33 @@ func TestHandlePatchConfig_UpdatesWebSearchAPIKeys(t *testing.T) { runConfigRequest(t, configPath, http.MethodPatch, `{ "tools": { "web": { + "brave": { "api_key": "brave-updated-key" }, + "tavily": { "api_key": "tavily-updated-key" }, + "perplexity": { "api_key": "perplexity-updated-key" }, "glm_search": { "api_key": "glm-updated-key" }, "baidu_search": { "api_key": "baidu-updated-key" } } } }`) - assertWebSearchAPIKeys(t, configPath, "glm-updated-key", "baidu-updated-key") + assertWebSearchAPIKeys(t, configPath, webSearchAPIKeys{ + brave: "brave-updated-key", + tavily: "tavily-updated-key", + perplexity: "perplexity-updated-key", + glm: "glm-updated-key", + baidu: "baidu-updated-key", + }) } func TestHandleUpdateConfig_PreservesWebSearchAPIKeysWhenOmitted(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() - seedWebSearchAPIKeys(t, configPath, "glm-existing-key-via-put", "baidu-existing-key-via-put") + seedWebSearchAPIKeys(t, configPath, webSearchAPIKeys{ + brave: "brave-existing-key-via-put", + tavily: "tavily-existing-key-via-put", + perplexity: "perplexity-existing-key-via-put", + glm: "glm-existing-key-via-put", + baidu: "baidu-existing-key-via-put", + }) runConfigRequest(t, configPath, http.MethodPut, `{ "version": 1, @@ -224,7 +268,13 @@ func TestHandleUpdateConfig_PreservesWebSearchAPIKeysWhenOmitted(t *testing.T) { } ] }`) - assertWebSearchAPIKeys(t, configPath, "glm-existing-key-via-put", "baidu-existing-key-via-put") + assertWebSearchAPIKeys(t, configPath, webSearchAPIKeys{ + brave: "brave-existing-key-via-put", + tavily: "tavily-existing-key-via-put", + perplexity: "perplexity-existing-key-via-put", + glm: "glm-existing-key-via-put", + baidu: "baidu-existing-key-via-put", + }) } func TestHandleUpdateConfig_UpdatesWebSearchAPIKeys(t *testing.T) { @@ -248,15 +298,24 @@ func TestHandleUpdateConfig_UpdatesWebSearchAPIKeys(t *testing.T) { ], "tools": { "web": { + "brave": { "api_key": "brave-updated-key-via-put" }, + "tavily": { "api_key": "tavily-updated-key-via-put" }, + "perplexity": { "api_key": "perplexity-updated-key-via-put" }, "glm_search": { "api_key": "glm-updated-key-via-put" }, "baidu_search": { "api_key": "baidu-updated-key-via-put" } } } }`) - assertWebSearchAPIKeys(t, configPath, "glm-updated-key-via-put", "baidu-updated-key-via-put") + assertWebSearchAPIKeys(t, configPath, webSearchAPIKeys{ + brave: "brave-updated-key-via-put", + tavily: "tavily-updated-key-via-put", + perplexity: "perplexity-updated-key-via-put", + glm: "glm-updated-key-via-put", + baidu: "baidu-updated-key-via-put", + }) } -func seedWebSearchAPIKeys(t *testing.T, configPath, glmAPIKey, baiduAPIKey string) { +func seedWebSearchAPIKeys(t *testing.T, configPath string, keys webSearchAPIKeys) { t.Helper() cfg, err := config.LoadConfig(configPath) @@ -264,8 +323,11 @@ func seedWebSearchAPIKeys(t *testing.T, configPath, glmAPIKey, baiduAPIKey strin t.Fatalf("LoadConfig() error = %v", err) } - cfg.Tools.Web.GLMSearch.SetAPIKey(glmAPIKey) - cfg.Tools.Web.BaiduSearch.SetAPIKey(baiduAPIKey) + cfg.Tools.Web.Brave.SetAPIKey(keys.brave) + cfg.Tools.Web.Tavily.SetAPIKey(keys.tavily) + cfg.Tools.Web.Perplexity.SetAPIKey(keys.perplexity) + cfg.Tools.Web.GLMSearch.SetAPIKey(keys.glm) + cfg.Tools.Web.BaiduSearch.SetAPIKey(keys.baidu) if err := config.SaveConfig(configPath, cfg); err != nil { t.Fatalf("SaveConfig() error = %v", err) diff --git a/web/frontend/src/components/config/config-page.tsx b/web/frontend/src/components/config/config-page.tsx index 2a246ab3e..e2d25e84e 100644 --- a/web/frontend/src/components/config/config-page.tsx +++ b/web/frontend/src/components/config/config-page.tsx @@ -183,6 +183,9 @@ export function ConfigPage() { "Cron exec timeout", { min: 0 }, ) + const braveAPIKey = form.braveAPIKey.trim() + const tavilyAPIKey = form.tavilyAPIKey.trim() + const perplexityAPIKey = form.perplexityAPIKey.trim() const glmSearchAPIKey = form.glmSearchAPIKey.trim() const baiduSearchAPIKey = form.baiduSearchAPIKey.trim() const execConfigPatch: Record = { @@ -209,6 +212,15 @@ export function ConfigPage() { } } + if (braveAPIKey !== "") { + webConfigPatch.brave = { api_key: braveAPIKey } + } + if (tavilyAPIKey !== "") { + webConfigPatch.tavily = { api_key: tavilyAPIKey } + } + if (perplexityAPIKey !== "") { + webConfigPatch.perplexity = { api_key: perplexityAPIKey } + } if (glmSearchAPIKey !== "") { webConfigPatch.glm_search = { api_key: glmSearchAPIKey } } diff --git a/web/frontend/src/components/config/config-sections.tsx b/web/frontend/src/components/config/config-sections.tsx index 8090b9f01..33bdc9228 100644 --- a/web/frontend/src/components/config/config-sections.tsx +++ b/web/frontend/src/components/config/config-sections.tsx @@ -521,6 +521,45 @@ export function WebSearchSection({ title={t("pages.config.sections.web_search")} description={t("pages.config.web_search_section_hint")} > + + onFieldChange("braveAPIKey", e.target.value)} + /> + + + + onFieldChange("tavilyAPIKey", e.target.value)} + /> + + + + onFieldChange("perplexityAPIKey", e.target.value)} + /> + +