fix(web/config): preserve and update builtin web provider API keys

This commit is contained in:
Alix-007 2026-03-27 01:32:34 +08:00
parent 935d5f8660
commit 9db32d996b
7 changed files with 181 additions and 14 deletions

View file

@ -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))
}

View file

@ -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)

View file

@ -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<string, unknown> = {
@ -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 }
}

View file

@ -521,6 +521,45 @@ export function WebSearchSection({
title={t("pages.config.sections.web_search")}
description={t("pages.config.web_search_section_hint")}
>
<Field
label={t("pages.config.brave_api_key")}
hint={t("pages.config.brave_api_key_hint")}
layout="setting-row"
>
<Input
type="password"
value={form.braveAPIKey}
placeholder={t("pages.config.secret_placeholder")}
onChange={(e) => onFieldChange("braveAPIKey", e.target.value)}
/>
</Field>
<Field
label={t("pages.config.tavily_api_key")}
hint={t("pages.config.tavily_api_key_hint")}
layout="setting-row"
>
<Input
type="password"
value={form.tavilyAPIKey}
placeholder={t("pages.config.secret_placeholder")}
onChange={(e) => onFieldChange("tavilyAPIKey", e.target.value)}
/>
</Field>
<Field
label={t("pages.config.perplexity_api_key")}
hint={t("pages.config.perplexity_api_key_hint")}
layout="setting-row"
>
<Input
type="password"
value={form.perplexityAPIKey}
placeholder={t("pages.config.secret_placeholder")}
onChange={(e) => onFieldChange("perplexityAPIKey", e.target.value)}
/>
</Field>
<Field
label={t("pages.config.glm_search_api_key")}
hint={t("pages.config.glm_search_api_key_hint")}

View file

@ -14,6 +14,9 @@ export interface CoreConfigForm {
execTimeoutSeconds: string
allowCommand: boolean
cronExecTimeoutMinutes: string
braveAPIKey: string
tavilyAPIKey: string
perplexityAPIKey: string
glmSearchAPIKey: string
baiduSearchAPIKey: string
maxTokens: string
@ -79,6 +82,9 @@ export const EMPTY_FORM: CoreConfigForm = {
execTimeoutSeconds: "0",
allowCommand: true,
cronExecTimeoutMinutes: "5",
braveAPIKey: "",
tavilyAPIKey: "",
perplexityAPIKey: "",
glmSearchAPIKey: "",
baiduSearchAPIKey: "",
maxTokens: "32768",
@ -133,6 +139,9 @@ export function buildFormFromConfig(config: unknown): CoreConfigForm {
const devices = asRecord(root.devices)
const tools = asRecord(root.tools)
const web = asRecord(tools.web)
const brave = asRecord(web.brave)
const tavily = asRecord(web.tavily)
const perplexity = asRecord(web.perplexity)
const glmSearch = asRecord(web.glm_search)
const baiduSearch = asRecord(web.baidu_search)
const cron = asRecord(tools.cron)
@ -191,6 +200,9 @@ export function buildFormFromConfig(config: unknown): CoreConfigForm {
cron.exec_timeout_minutes,
EMPTY_FORM.cronExecTimeoutMinutes,
),
braveAPIKey: asString(brave.api_key),
tavilyAPIKey: asString(tavily.api_key),
perplexityAPIKey: asString(perplexity.api_key),
glmSearchAPIKey: asString(glmSearch.api_key),
baiduSearchAPIKey: asString(baiduSearch.api_key),
maxTokens: asNumberString(defaults.max_tokens, EMPTY_FORM.maxTokens),

View file

@ -479,6 +479,12 @@
"cron_exec_timeout": "Scheduled Command Timeout (minutes)",
"cron_exec_timeout_hint": "Maximum runtime for scheduled commands. Set to 0 to disable the timeout.",
"web_search_section_hint": "Manage API keys for built-in web search providers. Leave a field blank to keep the current key unchanged.",
"brave_api_key": "Brave Search API Key",
"brave_api_key_hint": "Used by the built-in Brave web search provider.",
"tavily_api_key": "Tavily Search API Key",
"tavily_api_key_hint": "Used by the built-in Tavily web search provider.",
"perplexity_api_key": "Perplexity Search API Key",
"perplexity_api_key_hint": "Used by the built-in Perplexity web search provider.",
"glm_search_api_key": "GLM Search API Key",
"glm_search_api_key_hint": "Used by the built-in GLM web search provider.",
"baidu_search_api_key": "Baidu Search API Key",

View file

@ -479,6 +479,12 @@
"cron_exec_timeout": "定时命令超时(分钟)",
"cron_exec_timeout_hint": "定时任务中命令的最长运行时间。设置为 0 表示不限制超时。",
"web_search_section_hint": "管理内置网页搜索服务的 API Key。留空表示保持当前 Key 不变。",
"brave_api_key": "Brave 搜索 API Key",
"brave_api_key_hint": "用于内置的 Brave 网页搜索服务。",
"tavily_api_key": "Tavily 搜索 API Key",
"tavily_api_key_hint": "用于内置的 Tavily 网页搜索服务。",
"perplexity_api_key": "Perplexity 搜索 API Key",
"perplexity_api_key_hint": "用于内置的 Perplexity 网页搜索服务。",
"glm_search_api_key": "GLM 搜索 API Key",
"glm_search_api_key_hint": "用于内置的 GLM 网页搜索服务。",
"baidu_search_api_key": "百度搜索 API Key",