feat: 自建 SearXNG 搜索超时可配置(SEARXNG_TIMEOUT_SECONDS) (#2249)

* feat: make self-hosted SearXNG search timeout configurable

SEARXNG_TIMEOUT_SECONDS (default 10s, min 1) now controls the per-search
timeout for self-hosted SearXNG instances, threaded through all three
SearchService construction sites. Public-instance timeout stays fixed.

* fix: thread SEARXNG_TIMEOUT_SECONDS through subprocess rebuild and Actions

- persist searxng_timeout_seconds in _constructor_kwargs so the bounded
  topic-news subprocess rebuild no longer silently falls back to 10s
- map SEARXNG_TIMEOUT_SECONDS (vars||secrets) in 00-daily-analysis.yml
- document the variable in full-guide/full-guide_EN data-source tables,
  add CHANGELOG entry, and add regression tests for both paths

* fix: document SearXNG timeout environment contract

* fix: register SearXNG timeout setting

* fix: keep SearXNG timeout env-only

---------

Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
Co-authored-by: zhulinsen <zhuls97@163.com>
This commit is contained in:
TimingAlmostMet
2026-08-30 21:31:26 +08:00
committed by GitHub
parent 2ab6f147f3
commit ac8bd29b0f
11 changed files with 69 additions and 2 deletions

View File

@@ -983,6 +983,7 @@ class Config:
serpapi_keys: List[str] = field(default_factory=list) # SerpAPI Keys
searxng_base_urls: List[str] = field(default_factory=list) # SearXNG instance URLs (self-hosted, no quota)
searxng_public_instances_enabled: bool = False # Opt in to public discovery when base URLs are absent
searxng_timeout_seconds: int = 10 # 自建 SearXNG 单次搜索超时(秒)
# === Social Sentiment (US stocks only, api.adanos.org) ===
social_sentiment_api_key: Optional[str] = None
@@ -1880,6 +1881,9 @@ class Config:
serpapi_keys=serpapi_keys,
searxng_base_urls=searxng_base_urls,
searxng_public_instances_enabled=searxng_public_instances_enabled,
searxng_timeout_seconds=parse_env_int(
os.getenv('SEARXNG_TIMEOUT_SECONDS'), 10, field_name='SEARXNG_TIMEOUT_SECONDS', minimum=1
),
social_sentiment_api_key=os.getenv('SOCIAL_SENTIMENT_API_KEY') or None,
social_sentiment_api_url=os.getenv('SOCIAL_SENTIMENT_API_URL', 'https://api.adanos.org').rstrip('/'),
news_max_age_days=parse_env_int(os.getenv('NEWS_MAX_AGE_DAYS'), 3, field_name='NEWS_MAX_AGE_DAYS', minimum=1),

View File

@@ -79,6 +79,7 @@ WEB_SETTINGS_HIDDEN_FROM_UI = {
"USE_PROXY",
"PROXY_HOST",
"PROXY_PORT",
"SEARXNG_TIMEOUT_SECONDS",
}
_FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {

View File

@@ -105,6 +105,7 @@ def build_market_review_runtime(
"searxng_public_instances_enabled",
False,
),
searxng_timeout_seconds=getattr(config, "searxng_timeout_seconds", None),
news_max_age_days=getattr(config, "news_max_age_days", 3),
news_strategy_profile=getattr(config, "news_strategy_profile", "short"),
)

View File

@@ -309,6 +309,7 @@ class StockAnalysisPipeline:
minimax_keys=self.config.minimax_api_keys,
searxng_base_urls=self.config.searxng_base_urls,
searxng_public_instances_enabled=self.config.searxng_public_instances_enabled,
searxng_timeout_seconds=getattr(self.config, "searxng_timeout_seconds", None),
news_max_age_days=self.config.news_max_age_days,
news_strategy_profile=getattr(self.config, "news_strategy_profile", "short"),
)

View File

@@ -1843,10 +1843,20 @@ class SearXNGSearchProvider(BaseSearchProvider):
_public_instances_stale_retry_after: float = 0.0
_public_instances_lock = threading.Lock()
def __init__(self, base_urls: Optional[List[str]] = None, *, use_public_instances: bool = False):
def __init__(
self,
base_urls: Optional[List[str]] = None,
*,
use_public_instances: bool = False,
self_hosted_timeout_seconds: Optional[int] = None,
):
normalized_base_urls = [url.rstrip("/") for url in (base_urls or []) if url.strip()]
super().__init__(normalized_base_urls, "SearXNG")
self._base_urls = normalized_base_urls
if self_hosted_timeout_seconds and int(self_hosted_timeout_seconds) > 0:
self._self_hosted_timeout_seconds = int(self_hosted_timeout_seconds)
else:
self._self_hosted_timeout_seconds = self.SELF_HOSTED_TIMEOUT_SECONDS
self._use_public_instances = bool(use_public_instances and not self._base_urls)
self._cursor = 0
self._cursor_lock = threading.Lock()
@@ -2157,7 +2167,7 @@ class SearXNGSearchProvider(BaseSearchProvider):
max_attempts=len(self._base_urls),
)
retry_enabled = True
timeout = self.SELF_HOSTED_TIMEOUT_SECONDS
timeout = self._self_hosted_timeout_seconds
empty_error = "SearXNG 未配置可用实例"
elif self._use_public_instances:
public_instances = self._get_public_instances()
@@ -2390,6 +2400,7 @@ class SearchService:
minimax_keys: Optional[List[str]] = None,
searxng_base_urls: Optional[List[str]] = None,
searxng_public_instances_enabled: bool = False,
searxng_timeout_seconds: Optional[int] = None,
news_max_age_days: int = 3,
news_strategy_profile: str = "short",
):
@@ -2417,6 +2428,7 @@ class SearchService:
"minimax_keys": list(minimax_keys or []),
"searxng_base_urls": list(searxng_base_urls or []),
"searxng_public_instances_enabled": bool(searxng_public_instances_enabled),
"searxng_timeout_seconds": searxng_timeout_seconds,
"news_max_age_days": int(news_max_age_days),
"news_strategy_profile": news_strategy_profile,
}
@@ -2468,6 +2480,7 @@ class SearchService:
searxng_provider = SearXNGSearchProvider(
searxng_base_urls,
use_public_instances=bool(searxng_public_instances_enabled and not searxng_base_urls),
self_hosted_timeout_seconds=searxng_timeout_seconds,
)
if searxng_provider.is_available:
self._providers.append(searxng_provider)
@@ -4890,6 +4903,7 @@ def get_search_service() -> SearchService:
minimax_keys=config.minimax_api_keys,
searxng_base_urls=config.searxng_base_urls,
searxng_public_instances_enabled=config.searxng_public_instances_enabled,
searxng_timeout_seconds=getattr(config, "searxng_timeout_seconds", None),
news_max_age_days=config.news_max_age_days,
news_strategy_profile=getattr(config, "news_strategy_profile", "short"),
)