From 7dc2b643156178398d3ffc56c7271cd74c480284 Mon Sep 17 00:00:00 2001 From: ak Date: Fri, 25 Sep 2026 23:47:33 -0700 Subject: [PATCH] Request Search Assist explicitly with DuckDuckGo assist route --- docs/plugins.md | 6 ++++++ plugins/ask.go | 18 +++++++++++++----- plugins/ask_resilience_test.go | 16 ++++++++++++++++ plugins/ask_test.go | 2 +- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 1c739e8..6adce7e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1354,6 +1354,12 @@ waits for answer text with a source link instead of stopping at the empty Search Assist heading, and supports expanded answer cards. Browser requests share one slot across networks to limit memory use on small hosts. +Both the lightweight and browser lookups explicitly request Search Assist using +the `assiston=1` web route published in DuckDuckGo's [official bang catalog](https://duckduckgo.com/bang.js) +for `!assist`. The no-answer search link requests it too. This avoids relying on +the normal search page's automatic answer-display setting, but is not a public +Search Assist API, a challenge bypass, or a guarantee of an answer for every query. + The plugin is configured under `plugins.ask`: ~~~yaml diff --git a/plugins/ask.go b/plugins/ask.go index 27df97f..10c73ee 100644 --- a/plugins/ask.go +++ b/plugins/ask.go @@ -780,7 +780,7 @@ func askDuckDuckGoSearchAssist(ctx context.Context, question string) (askSource, if attempt < len(queries) { query = queries[attempt] } - if source, ok := askDuckDuckGoSearchAssistOnce(ctx, query, duckDuckGoSearchURL(question)); ok { + if source, ok := askDuckDuckGoSearchAssistOnce(ctx, query, duckDuckGoSearchAssistURL(question)); ok { return source, true } if err := ctx.Err(); err != nil { @@ -830,7 +830,7 @@ func askSearchAssistQueryVariants(question string) []string { var askVersusPattern = regexp.MustCompile(`(?i)\bvs\b\.?`) func askDuckDuckGoSearchAssistOnce(ctx context.Context, question, fallbackURL string) (askSource, bool) { - pageURL := duckDuckGoSearchURL(question) + pageURL := duckDuckGoSearchAssistURL(question) req, err := http.NewRequestWithContext(ctx, http.MethodGet, pageURL, nil) if err != nil { return askSource{}, false @@ -946,7 +946,7 @@ func askDuckDuckGoRenderedSearchAssist(ctx context.Context, question, browserPat if index < len(queries) { query = queries[index] } - if source, ok := askDuckDuckGoRenderedSearchAssistOnce(ctx, query, duckDuckGoSearchURL(question), executable, fetchResults); ok { + if source, ok := askDuckDuckGoRenderedSearchAssistOnce(ctx, query, duckDuckGoSearchAssistURL(question), executable, fetchResults); ok { return source, true } if err := ctx.Err(); err != nil { @@ -986,7 +986,7 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba defer browserCancel() var result askRenderedSearchAssistData - if err := chromedp.Run(browserCtx, chromedp.Navigate(duckDuckGoSearchURL(question))); err != nil { + if err := chromedp.Run(browserCtx, chromedp.Navigate(duckDuckGoSearchAssistURL(question))); err != nil { return askSource{}, false } // Navigation and browser startup have already consumed some of the budget. @@ -1484,6 +1484,14 @@ func duckDuckGoSearchURL(query string) string { return "https://duckduckgo.com/?q=" + url.QueryEscape(strings.TrimSpace(query)) } +// This is the destination used by DuckDuckGo's official !assist bang +// (https://duckduckgo.com/bang.js). It requests Search Assist explicitly, +// rather than relying on the default search setting to display an answer. +// It is a web route, not an API or a way to bypass provider challenges. +func duckDuckGoSearchAssistURL(query string) string { + return "https://duckduckgo.com/?assiston=1&q=" + url.QueryEscape(strings.TrimSpace(query)) +} + func askHTTPSuccess(status int) bool { return status >= http.StatusOK && status < http.StatusMultipleChoices } @@ -2056,7 +2064,7 @@ func formatAskResponse(nick, answer, sourceURL string, maxLength, maxResponseCha func formatAskNoAnswer(question string, maxLength int) string { maxLength = clampAskLength(maxLength, 120, 450, 360) - return truncateAskBytes("I couldn't find a reliable answer — search: "+duckDuckGoSearchURL(question), maxLength) + return truncateAskBytes("I couldn't find a reliable answer — search: "+duckDuckGoSearchAssistURL(question), maxLength) } func clampAskLength(value, min, max, fallback int) int { diff --git a/plugins/ask_resilience_test.go b/plugins/ask_resilience_test.go index b2b38e2..7a4bb6b 100644 --- a/plugins/ask_resilience_test.go +++ b/plugins/ask_resilience_test.go @@ -3,6 +3,7 @@ package plugins import ( "context" "net/http" + "net/url" "os" "reflect" "strings" @@ -72,6 +73,9 @@ func TestAskComparisonRecoversOnNormalizedRetry(t *testing.T) { if r.URL.Path == "/assist.js" { return newPluginResponse(200, `DDG.deep.deepPayload={"instantAnswers":[{"data":{"answer":"Compare equivalent dirt bikes and go-karts, including maintenance costs.","sources":[{"article":{"link":"https://example.org/comparison"}}]}}]};callback();`), nil } + if r.URL.Query().Get("assiston") != "1" { + t.Fatal("Search Assist was not explicitly requested") + } queries = append(queries, r.URL.Query().Get("q")) if len(queries) == 1 { return newPluginResponse(200, `No answer yet`), nil @@ -87,6 +91,18 @@ func TestAskComparisonRecoversOnNormalizedRetry(t *testing.T) { } } +func TestAskExplicitAssistURL(t *testing.T) { + for _, question := range []string{"how much do dirt bikes cost vs GoKarts?", "R&D vs C++? &assiston=0", "日本語の質問"} { + u, err := url.Parse(duckDuckGoSearchAssistURL(question)) + if err != nil || u.Host != "duckduckgo.com" || u.Scheme != "https" || u.Query().Get("q") != question || !reflect.DeepEqual(u.Query()["assiston"], []string{"1"}) { + t.Fatalf("invalid explicit route: %v %v", u, err) + } + } + if !strings.Contains(formatAskNoAnswer("a question", 360), "?assiston=1&q=a+question") { + t.Fatal("fallback link does not request Search Assist") + } +} + func TestAskComparisonDoesNotReturnEntityDescription(t *testing.T) { old := askHTTPClient t.Cleanup(func() { askHTTPClient = old }) diff --git a/plugins/ask_test.go b/plugins/ask_test.go index f77ef2d..7bee0f0 100644 --- a/plugins/ask_test.go +++ b/plugins/ask_test.go @@ -484,7 +484,7 @@ func TestAskResponseDropsInvalidSourceURL(t *testing.T) { func TestAskNoAnswerProvidesSearchFallback(t *testing.T) { got := formatAskNoAnswer("Is the Earth flat?", 180) - if !strings.Contains(got, "I couldn't find a reliable answer") || !strings.Contains(got, "https://duckduckgo.com/?q=Is+the+Earth+flat%3F") { + if !strings.Contains(got, "I couldn't find a reliable answer") || !strings.Contains(got, "https://duckduckgo.com/?assiston=1&q=Is+the+Earth+flat%3F") { t.Fatalf("formatAskNoAnswer() = %q, want bounded search fallback", got) } if strings.ContainsAny(got, "\r\n") || len([]byte(got)) > 180 {