From 293477b02a13bb47515c8ec1dca23150844e6b5b Mon Sep 17 00:00:00 2001 From: Junghwan <70629228+shaun0927@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:45:25 +0900 Subject: [PATCH] Keep launcher locale changes from mutating shared web-search routing (#2573) The launcher wired UI language changes into a process-global backend switch that changed auto web-search provider selection and the reported current service for every handler in the same process. This narrows the fix to the validated leak: remove backend sync from frontend locale changes, drop the now-unused UI endpoint, and make auto selection fall back to a stable default when the query itself does not contain a script hint. Constraint: Keep the patch small and mergeable without redesigning per-user preference storage Rejected: Add per-user backend language state | larger scope than the validated bug and unclear maintainer preference Rejected: Persist preferred language in config | still shares mutable state across clients of the same instance Confidence: high Scope-risk: narrow Reversibility: clean Directive: If locale-aware provider routing is reintroduced later, scope it to explicit config or request context instead of package-global state Tested: go test ./web/backend/api ./pkg/tools -count=1; pnpm lint; pnpm build Not-tested: Full make check; live multi-browser manual launcher run after the backend endpoint removal --- pkg/tools/integration/web.go | 27 ++--------------- pkg/tools/integration/web_test.go | 19 ++---------- pkg/tools/integration_facade.go | 8 ------ web/backend/api/router.go | 1 - web/backend/api/tools_test.go | 13 +-------- web/backend/api/ui.go | 27 ----------------- web/backend/api/ui_test.go | 48 ------------------------------- web/backend/main.go | 2 -- web/frontend/src/i18n/index.ts | 10 ------- 9 files changed, 5 insertions(+), 150 deletions(-) delete mode 100644 web/backend/api/ui.go delete mode 100644 web/backend/api/ui_test.go diff --git a/pkg/tools/integration/web.go b/pkg/tools/integration/web.go index 56663ecda..75821e40d 100644 --- a/pkg/tools/integration/web.go +++ b/pkg/tools/integration/web.go @@ -58,8 +58,6 @@ var ( reSogouRealURL = regexp.MustCompile(`url=([^&]+)`) ) -var preferredWebSearchLanguage atomic.Value - type APIKeyPool struct { keys []string current uint32 @@ -250,27 +248,6 @@ func mapBaiduRecencyFilter(rangeCode string) string { } } -func normalizePreferredWebSearchLanguage(lang string) string { - lang = strings.ToLower(strings.TrimSpace(lang)) - switch { - case strings.HasPrefix(lang, "zh"), lang == "chinese": - return "zh" - case strings.HasPrefix(lang, "en"), lang == "english": - return "en" - default: - return "" - } -} - -func SetPreferredWebSearchLanguage(lang string) { - preferredWebSearchLanguage.Store(normalizePreferredWebSearchLanguage(lang)) -} - -func GetPreferredWebSearchLanguage() string { - lang, _ := preferredWebSearchLanguage.Load().(string) - return lang -} - type BraveSearchProvider struct { keyPool *APIKeyPool proxy string @@ -1420,7 +1397,7 @@ func containsLatinLetter(text string) bool { func prefersDuckDuckGoQuery(text string) bool { trimmed := strings.TrimSpace(text) if trimmed == "" { - return GetPreferredWebSearchLanguage() == "en" + return false } if containsHan(trimmed) { return false @@ -1428,7 +1405,7 @@ func prefersDuckDuckGoQuery(text string) bool { if containsLatinLetter(trimmed) { return true } - return GetPreferredWebSearchLanguage() == "en" + return false } func (opts WebSearchToolOptions) buildProviderResolver() (func(query string) (SearchProvider, int), error) { diff --git a/pkg/tools/integration/web_test.go b/pkg/tools/integration/web_test.go index d47d8e7c9..ba6b3da45 100644 --- a/pkg/tools/integration/web_test.go +++ b/pkg/tools/integration/web_test.go @@ -1778,11 +1778,6 @@ func TestApplySogouRangeHint(t *testing.T) { } func TestPrefersDuckDuckGoQuery(t *testing.T) { - SetPreferredWebSearchLanguage("") - t.Cleanup(func() { - SetPreferredWebSearchLanguage("") - }) - tests := []struct { name string query string @@ -1805,19 +1800,9 @@ func TestPrefersDuckDuckGoQuery(t *testing.T) { } } -func TestPrefersDuckDuckGoQuery_FallsBackToPreferredLanguage(t *testing.T) { - SetPreferredWebSearchLanguage("en") - t.Cleanup(func() { - SetPreferredWebSearchLanguage("") - }) - - if !prefersDuckDuckGoQuery("2026 04 15") { - t.Fatal("numeric query should prefer DuckDuckGo when preferred language is English") - } - - SetPreferredWebSearchLanguage("zh") +func TestPrefersDuckDuckGoQuery_DoesNotUseGlobalLanguageFallback(t *testing.T) { if prefersDuckDuckGoQuery("2026 04 15") { - t.Fatal("numeric query should prefer Sogou when preferred language is Chinese") + t.Fatal("numeric query should default to Sogou when no script-specific hint is present") } } diff --git a/pkg/tools/integration_facade.go b/pkg/tools/integration_facade.go index b05a22fe2..193ecd6f5 100644 --- a/pkg/tools/integration_facade.go +++ b/pkg/tools/integration_facade.go @@ -65,14 +65,6 @@ func NewAPIKeyPool(keys []string) *APIKeyPool { return integrationtools.NewAPIKeyPool(keys) } -func SetPreferredWebSearchLanguage(lang string) { - integrationtools.SetPreferredWebSearchLanguage(lang) -} - -func GetPreferredWebSearchLanguage() string { - return integrationtools.GetPreferredWebSearchLanguage() -} - func WebSearchToolOptionsFromConfig(cfg *config.Config) WebSearchToolOptions { return integrationtools.WebSearchToolOptionsFromConfig(cfg) } diff --git a/web/backend/api/router.go b/web/backend/api/router.go index f4ac78ab4..76f63607e 100644 --- a/web/backend/api/router.go +++ b/web/backend/api/router.go @@ -89,7 +89,6 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // Skills and tools support/actions h.registerSkillRoutes(mux) h.registerToolRoutes(mux) - h.registerUIRoutes(mux) // OS startup / launch-at-login h.registerStartupRoutes(mux) diff --git a/web/backend/api/tools_test.go b/web/backend/api/tools_test.go index ffeae9b64..c98067e41 100644 --- a/web/backend/api/tools_test.go +++ b/web/backend/api/tools_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/sipeed/picoclaw/pkg/config" - picotools "github.com/sipeed/picoclaw/pkg/tools" ) func TestHandleListTools(t *testing.T) { @@ -517,22 +516,12 @@ func TestResolveCurrentWebSearchProvider_FallsBackWhenProviderIsUnknown(t *testi } } -func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuckGo(t *testing.T) { +func TestResolveCurrentWebSearchProvider_PrefersStableDefaultForSogouAndDuckDuckGo(t *testing.T) { cfg := config.DefaultConfig() cfg.Tools.Web.Provider = "auto" cfg.Tools.Web.Sogou.Enabled = true cfg.Tools.Web.DuckDuckGo.Enabled = true - picotools.SetPreferredWebSearchLanguage("en") - t.Cleanup(func() { - picotools.SetPreferredWebSearchLanguage("") - }) - - if got := resolveCurrentWebSearchProvider(cfg); got != "duckduckgo" { - t.Fatalf("resolveCurrentWebSearchProvider() = %q, want duckduckgo", got) - } - - picotools.SetPreferredWebSearchLanguage("zh") if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" { t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got) } diff --git a/web/backend/api/ui.go b/web/backend/api/ui.go deleted file mode 100644 index 90d96403e..000000000 --- a/web/backend/api/ui.go +++ /dev/null @@ -1,27 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/sipeed/picoclaw/pkg/tools" -) - -type uiLanguageRequest struct { - Language string `json:"language"` -} - -func (h *Handler) registerUIRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /api/ui/language", h.handleSetUILanguage) -} - -func (h *Handler) handleSetUILanguage(w http.ResponseWriter, r *http.Request) { - var req uiLanguageRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - tools.SetPreferredWebSearchLanguage(req.Language) - w.WriteHeader(http.StatusNoContent) -} diff --git a/web/backend/api/ui_test.go b/web/backend/api/ui_test.go deleted file mode 100644 index 3de35b7cb..000000000 --- a/web/backend/api/ui_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package api - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/sipeed/picoclaw/pkg/tools" -) - -func TestHandleSetUILanguage(t *testing.T) { - tools.SetPreferredWebSearchLanguage("") - t.Cleanup(func() { - tools.SetPreferredWebSearchLanguage("") - }) - - h := NewHandler("") - mux := http.NewServeMux() - h.RegisterRoutes(mux) - - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/api/ui/language", strings.NewReader(`{"language":"zh"}`)) - req.Header.Set("Content-Type", "application/json") - mux.ServeHTTP(rec, req) - - if rec.Code != http.StatusNoContent { - t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusNoContent, rec.Body.String()) - } - if got := tools.GetPreferredWebSearchLanguage(); got != "zh" { - t.Fatalf("preferred web search language = %q, want zh", got) - } -} - -func TestHandleSetUILanguage_RejectsInvalidJSON(t *testing.T) { - h := NewHandler("") - mux := http.NewServeMux() - h.RegisterRoutes(mux) - - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/api/ui/language", strings.NewReader(`{`)) - req.Header.Set("Content-Type", "application/json") - mux.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) - } -} diff --git a/web/backend/main.go b/web/backend/main.go index f5362174b..fa2448d5c 100644 --- a/web/backend/main.go +++ b/web/backend/main.go @@ -29,7 +29,6 @@ import ( "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/netbind" - "github.com/sipeed/picoclaw/pkg/tools" "github.com/sipeed/picoclaw/web/backend/api" "github.com/sipeed/picoclaw/web/backend/dashboardauth" "github.com/sipeed/picoclaw/web/backend/launcherconfig" @@ -409,7 +408,6 @@ func main() { if *lang != "" { SetLanguage(*lang) } - tools.SetPreferredWebSearchLanguage(string(GetLanguage())) // Resolve config path configPath := utils.GetDefaultConfigPath() diff --git a/web/frontend/src/i18n/index.ts b/web/frontend/src/i18n/index.ts index 5c3a26d48..bdc1fe917 100644 --- a/web/frontend/src/i18n/index.ts +++ b/web/frontend/src/i18n/index.ts @@ -7,8 +7,6 @@ import i18n from "i18next" import LanguageDetector from "i18next-browser-languagedetector" import { initReactI18next } from "react-i18next" -import { launcherFetch } from "@/api/http" - import en from "./locales/en.json" import zh from "./locales/zh.json" @@ -46,14 +44,6 @@ i18n.on("languageChanged", (lng) => { } else { dayjs.locale("en") } - - void launcherFetch("/api/ui/language", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ language: lng }), - }).catch(() => { - // Keep UI language changes responsive even if backend sync fails. - }) }) export default i18n