Skip to content

Commit 2e429c3

Browse files
author
pinvou3-dev
committed
fix(search): API 后端失败兜底从 DuckDuckGo 换成 Bing
DuckDuckGo 在中国大陆网络下被 DNS 污染 + SNI 重置,完全不可达; API 后端失败后撞 DDG 兜底等于必然整体失败。Bing 是唯一全球与 中国大陆均免 key 可达的引擎,链尾按可达性选择,不做地理探测。 全链失败错误补充 API 后端配置建议。 Signed-off-by: pinvou3-dev <dev@pinvou3.local>
1 parent 0d89a31 commit 2e429c3

3 files changed

Lines changed: 59 additions & 7 deletions

File tree

crates/tui/src/config/search.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
use serde::{Deserialize, Serialize};
99

1010
/// Search provider enumeration — selects the first backend `web_search` uses.
11-
/// API-backed providers may visibly degrade through the default DuckDuckGo →
12-
/// Bing chain after runtime failure or an empty result. Configuration and
11+
/// API-backed providers may visibly degrade through the keyless Bing tail
12+
/// after runtime failure or an empty result. Configuration and
1313
/// network-policy errors fail closed without crossing providers.
1414
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
1515
#[serde(rename_all = "snake_case")]

crates/tui/src/tools/web/backend.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,14 @@ impl<'a> SearchBackendChain<'a> {
113113
context, selected,
114114
)));
115115
if !matches!(selected, SearchProvider::Bing | SearchProvider::DuckDuckGo) {
116+
// The keyless tail must stay reachable from mainland-China
117+
// networks, where DuckDuckGo is DNS-poisoned and SNI-reset while
118+
// Bing serves both its global and China endpoints without a key.
119+
// The tail is picked by reachability, not by geo detection: the
120+
// engine never guesses the user's location.
116121
backends.push(Box::new(ConfiguredSearchBackend::from_provider(
117122
context,
118-
SearchProvider::DuckDuckGo,
123+
SearchProvider::Bing,
119124
)));
120125
}
121126
Self { backends }
@@ -242,7 +247,8 @@ async fn run_backend_chain(
242247
.collect::<Vec<_>>()
243248
.join(", ");
244249
Err(ToolError::not_available(format!(
245-
"web search backends unavailable: {backend_ids}"
250+
"web search backends unavailable: {backend_ids}; \
251+
configure an API-backed [search] provider (tavily, bocha, metaso, baidu, volcengine) for dependable results"
246252
)))
247253
}
248254

@@ -464,6 +470,47 @@ mod tests {
464470
}
465471
}
466472

473+
/// API-backed providers must fall back to the keyless Bing tail, not
474+
/// DuckDuckGo: DDG is unreachable from mainland-China networks
475+
/// (DNS poisoning + SNI reset), so a DDG tail turns every API outage
476+
/// into a guaranteed total failure there, while Bing stays reachable
477+
/// globally without a key. Bing and DuckDuckGo themselves stay
478+
/// single-backend chains (their internal fallbacks own that job).
479+
#[test]
480+
fn forkguard_api_provider_chain_tail_is_bing() {
481+
for selected in [
482+
SearchProvider::Tavily,
483+
SearchProvider::Bocha,
484+
SearchProvider::Metaso,
485+
SearchProvider::Baidu,
486+
SearchProvider::Searxng,
487+
SearchProvider::Volcengine,
488+
SearchProvider::Sofya,
489+
] {
490+
let mut context = ToolContext::new(std::path::PathBuf::from("."));
491+
context.search_provider = selected;
492+
let chain = SearchBackendChain::from_context(&context);
493+
let ids: Vec<BackendId> = chain.backends.iter().map(|backend| backend.id()).collect();
494+
let expected_tail = BackendId::Bing;
495+
assert_eq!(
496+
ids.last(),
497+
Some(&expected_tail),
498+
"{selected:?} chain must keep a reachable keyless Bing tail"
499+
);
500+
}
501+
502+
for selected in [SearchProvider::Bing, SearchProvider::DuckDuckGo] {
503+
let mut context = ToolContext::new(std::path::PathBuf::from("."));
504+
context.search_provider = selected;
505+
let chain = SearchBackendChain::from_context(&context);
506+
assert_eq!(
507+
chain.backends.len(),
508+
1,
509+
"{selected:?} must stay a single-backend chain"
510+
);
511+
}
512+
}
513+
467514
#[test]
468515
fn provider_native_is_fail_closed_without_both_fact_and_client() {
469516
assert!(!provider_native_is_available(false, false));
@@ -641,6 +688,7 @@ mod tests {
641688

642689
assert!(matches!(error, ToolError::NotAvailable { .. }));
643690
assert!(message.contains("bocha, duckduckgo"));
691+
assert!(message.contains("configure an API-backed [search] provider"));
644692
assert!(!message.contains(private_error));
645693
assert!(!message.contains("different private response"));
646694
}

docs/CONFIGURATION.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,9 +2274,13 @@ and Tavily, Bocha, Metaso, SearXNG, Baidu, Volcengine, or Sofya can be selected
22742274
when an API-backed provider is preferred.
22752275

22762276
Configured API providers are attempted first. Runtime failure or an empty
2277-
result visibly degrades through DuckDuckGo and then Bing; the structured search
2278-
receipt records every hop. Missing configuration and network-policy denials
2279-
fail closed without sending the query to another provider.
2277+
result visibly degrades to the keyless Bing tail. Bing is picked over
2278+
DuckDuckGo by reachability, not geo detection: Bing serves both its global and
2279+
China endpoints without a key, while DuckDuckGo is unreachable from
2280+
mainland-China networks. The structured search receipt records every hop.
2281+
Missing configuration and network-policy denials fail closed without sending
2282+
the query to another provider. When every backend is unavailable the tool
2283+
fails closed with an error suggesting API-backed `[search]` providers.
22802284

22812285
For a private/internal search service that serves DuckDuckGo-compatible HTML,
22822286
keep `provider = "duckduckgo"` and set `base_url`; Codewhale appends the `q`

0 commit comments

Comments
 (0)