From 33a37eda80da468d5b95fc2d3411733814387151 Mon Sep 17 00:00:00 2001 From: pinvou3-dev Date: Fri, 28 Aug 2026 17:13:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(search):=20API=20=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=85=9C=E5=BA=95=E4=BB=8E=20DuckDuckGo=20?= =?UTF-8?q?=E6=8D=A2=E6=88=90=20Bing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DuckDuckGo 在中国大陆网络下被 DNS 污染 + SNI 重置,完全不可达; API 后端失败后撞 DDG 兜底等于必然整体失败。Bing 是唯一全球与 中国大陆均免 key 可达的引擎,链尾按可达性选择,不做地理探测。 全链失败错误补充 API 后端配置建议。 Signed-off-by: pinvou3-dev --- crates/tui/src/config/search.rs | 4 +-- crates/tui/src/tools/web/backend.rs | 52 +++++++++++++++++++++++++++-- docs/CONFIGURATION.md | 10 ++++-- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/crates/tui/src/config/search.rs b/crates/tui/src/config/search.rs index 0827a4d642..a8ca085706 100644 --- a/crates/tui/src/config/search.rs +++ b/crates/tui/src/config/search.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; /// Search provider enumeration — selects the first backend `web_search` uses. -/// API-backed providers may visibly degrade through the default DuckDuckGo → -/// Bing chain after runtime failure or an empty result. Configuration and +/// API-backed providers may visibly degrade through the keyless Bing tail +/// after runtime failure or an empty result. Configuration and /// network-policy errors fail closed without crossing providers. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] diff --git a/crates/tui/src/tools/web/backend.rs b/crates/tui/src/tools/web/backend.rs index 3aa5b3ef5a..6ae36b0f40 100644 --- a/crates/tui/src/tools/web/backend.rs +++ b/crates/tui/src/tools/web/backend.rs @@ -113,9 +113,14 @@ impl<'a> SearchBackendChain<'a> { context, selected, ))); if !matches!(selected, SearchProvider::Bing | SearchProvider::DuckDuckGo) { + // The keyless tail must stay reachable from mainland-China + // networks, where DuckDuckGo is DNS-poisoned and SNI-reset while + // Bing serves both its global and China endpoints without a key. + // The tail is picked by reachability, not by geo detection: the + // engine never guesses the user's location. backends.push(Box::new(ConfiguredSearchBackend::from_provider( context, - SearchProvider::DuckDuckGo, + SearchProvider::Bing, ))); } Self { backends } @@ -255,7 +260,8 @@ async fn run_backend_chain( .collect::>() .join(", "); Err(ToolError::not_available(format!( - "web search backends unavailable: {backend_ids}" + "web search backends unavailable: {backend_ids}; \ + configure an API-backed [search] provider (tavily, bocha, metaso, baidu, volcengine) for dependable results" ))) } @@ -521,6 +527,47 @@ mod tests { } } + /// API-backed providers must fall back to the keyless Bing tail, not + /// DuckDuckGo: DDG is unreachable from mainland-China networks + /// (DNS poisoning + SNI reset), so a DDG tail turns every API outage + /// into a guaranteed total failure there, while Bing stays reachable + /// globally without a key. Bing and DuckDuckGo themselves stay + /// single-backend chains (their internal fallbacks own that job). + #[test] + fn forkguard_api_provider_chain_tail_is_bing() { + for selected in [ + SearchProvider::Tavily, + SearchProvider::Bocha, + SearchProvider::Metaso, + SearchProvider::Baidu, + SearchProvider::Searxng, + SearchProvider::Volcengine, + SearchProvider::Sofya, + ] { + let mut context = ToolContext::new(std::path::PathBuf::from(".")); + context.search_provider = selected; + let chain = SearchBackendChain::from_context(&context); + let ids: Vec = chain.backends.iter().map(|backend| backend.id()).collect(); + let expected_tail = BackendId::Bing; + assert_eq!( + ids.last(), + Some(&expected_tail), + "{selected:?} chain must keep a reachable keyless Bing tail" + ); + } + + for selected in [SearchProvider::Bing, SearchProvider::DuckDuckGo] { + let mut context = ToolContext::new(std::path::PathBuf::from(".")); + context.search_provider = selected; + let chain = SearchBackendChain::from_context(&context); + assert_eq!( + chain.backends.len(), + 1, + "{selected:?} must stay a single-backend chain" + ); + } + } + #[test] fn provider_native_is_fail_closed_without_both_fact_and_client() { assert!(!provider_native_is_available(false, false)); @@ -703,6 +750,7 @@ mod tests { assert!(matches!(error, ToolError::NotAvailable { .. })); assert!(message.contains("bocha, duckduckgo")); + assert!(message.contains("configure an API-backed [search] provider")); assert!(!message.contains(private_error)); assert!(!message.contains("different private response")); } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3bafa02780..fe247f894b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2272,13 +2272,17 @@ first makes a separate, bounded request through that provider's native search contract. It reuses the active route's authentication and transport, but does not inject provider-owned tools into the main conversation. A runtime failure or a response without usable citations then falls back visibly to the -configured `[search]` backend and, unless that backend is already Bing or -DuckDuckGo, to DuckDuckGo. Provider-native search receives a separate bounded +configured `[search]` backend and then to the keyless Bing tail. Bing is +picked over DuckDuckGo by reachability, not geo detection: Bing serves both +its global and China endpoints without a key, while DuckDuckGo is unreachable +from mainland-China networks. Provider-native search receives a separate bounded budget of at least 45 seconds instead of an equal share that shrinks as fallbacks are added. Direct Moonshot `kimi-k3` Formula search uses a 180-second floor because its bounded protocol can require several Chat Completions and Fiber steps; the caller's configured search timeout remains available to the -configured/local fallback. The structured search receipt records every hop. +configured/local fallback. The structured search receipt records every hop. When every backend is +unavailable the tool fails closed with an error suggesting API-backed +`[search]` providers. Without an eligible provider-native route, `web_search` uses DuckDuckGo by default and does not require an API key. The DuckDuckGo path keeps a Bing