From b740beea2ab33b2f2a7875d7c7d88083a2ce5478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E7=8E=89=E6=A1=88?= <2995111793@qq.com> Date: Wed, 19 Aug 2026 21:59:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=AC=E5=85=B1=20SearXNG=20=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E5=8F=91=E7=8E=B0=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD?= =?UTF-8?q?=20(#2226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 公共 SearXNG 实例发现默认关闭 公共实例普遍存在限流(429)、下线(503)或未开启 JSON 输出的情况, 默认开启会让未配置任何搜索 key 的用户每次分析多耗 30~60 秒, 且新闻面最终为空却不在报告中提示(详见 #2225 的实测数据)。 - .env.example: 默认值改为 false,并说明原因与推荐做法 - docs/full-guide.md、docs/full-guide_EN.md: 同步默认值与说明 - tests: 放宽段落定位断言以适配新默认值,并新增一条测试固化该默认值 * fix: 公共 SearXNG 实例发现改在运行时层面默认关闭 按 review 意见修正:仅改 .env.example 只影响复制新模板的用户, 已有安装与 GitHub Actions(未配置 Variable/Secret 时变量为空) 仍会走 default=True 的运行时分支,继续承受 30~60 秒失败重试。 - src/config.py: parse_env_bool 的 default 由 True 改为 False(真正生效处) - src/core/config_registry.py: 示例顺序调整为 false 优先 - .github/workflows/00-daily-analysis.yml: 变量未设置时诊断显示「默认关闭」, 与运行时行为保持一致,不再误报为已开启 - tests: 新增三条回归测试,分别锁住运行时默认值、config.py 源码中的 default=False、以及工作流诊断文案 - docs/CHANGELOG.md: 在 [Unreleased] 追加条目 显式设为 true 的用户行为不变。27 passed * fix: align SearXNG public instance defaults * fix: complete SearXNG opt-in defaults --------- Co-authored-by: Mach-Chan Co-authored-by: ZhuLinsen --- .env.example | 6 +- .github/workflows/00-daily-analysis.yml | 2 +- apps/dsa-web/src/locales/settingsHelp.test.ts | 10 ++ apps/dsa-web/src/utils/systemConfigI18n.ts | 2 +- docs/CHANGELOG.md | 1 + docs/DEPLOY.md | 4 +- docs/docker/zeabur-deployment.md | 4 +- docs/full-guide.md | 8 +- docs/full-guide_EN.md | 8 +- src/config.py | 4 +- src/core/config_registry.py | 6 +- src/core/market_review_runtime.py | 2 +- src/search_service.py | 2 +- tests/test_market_review_runtime.py | 18 ++++ tests/test_search_searxng.py | 7 ++ tests/test_searxng_env_docs.py | 97 ++++++++++++++++++- tests/test_system_config_service.py | 10 ++ 17 files changed, 167 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index dae0bc819..cb225f4b1 100644 --- a/.env.example +++ b/.env.example @@ -396,9 +396,11 @@ SERPAPI_API_KEYS= BRAVE_API_KEYS= # SearXNG 实例地址(逗号分隔,私有部署无配额,需在 settings.yml 启用 format: json) # GitHub Actions 每日分析工作流支持同名 Variables 优先、Secrets 回退;公网地址可配置为 Variable。 -# 留空时默认自动从 searx.space 拉取公共实例;若不希望访问公共实例,可将下方开关设为 false +# 留空且下方开关为 true 时,会自动从 searx.space 拉取公共实例。公共实例普遍存在限流(429)、 +# 下线(503)或未开启 JSON 输出的情况,逐个重试会让每次分析多耗 30~60 秒且新闻面最终为空, +# 因此默认关闭。推荐配置自建实例地址(SEARXNG_BASE_URLS),或使用上方任一带 key 的搜索渠道。 SEARXNG_BASE_URLS= -SEARXNG_PUBLIC_INSTANCES_ENABLED=true +SEARXNG_PUBLIC_INSTANCES_ENABLED=false # =================================== # Social Sentiment Intelligence (US stocks only) diff --git a/.github/workflows/00-daily-analysis.yml b/.github/workflows/00-daily-analysis.yml index c62e28a94..2a72fb1e4 100644 --- a/.github/workflows/00-daily-analysis.yml +++ b/.github/workflows/00-daily-analysis.yml @@ -497,7 +497,7 @@ jobs: echo " SearXNG Public Instances: ❌ 已禁用" ;; "") - echo " SearXNG Public Instances: ✅ 默认开启" + echo " SearXNG Public Instances: ⚪ 默认关闭(公共实例普遍限流,如需启用请显式设为 true)" ;; *) echo " SearXNG Public Instances: ✅ 已启用" diff --git a/apps/dsa-web/src/locales/settingsHelp.test.ts b/apps/dsa-web/src/locales/settingsHelp.test.ts index 8a898636c..6fe7d46a6 100644 --- a/apps/dsa-web/src/locales/settingsHelp.test.ts +++ b/apps/dsa-web/src/locales/settingsHelp.test.ts @@ -11,6 +11,16 @@ const flattenHelp = (help: ReturnType) => [ ...(help?.notes ?? []), ].filter(Boolean).join(' '); +describe('SearXNG settings help', () => { + it('describes public SearXNG discovery as opt-in in Chinese', () => { + const description = getFieldDescriptionZh('SEARXNG_PUBLIC_INSTANCES_ENABLED'); + + expect(description).toContain('默认关闭'); + expect(description).toContain('设为 true'); + expect(description).not.toContain('设为 false 可禁用该默认行为'); + }); +}); + describe('Skill Outcome auto-weight settings help', () => { it('describes the attributable Outcome threshold in Chinese', () => { const help = getSettingsHelpContent( diff --git a/apps/dsa-web/src/utils/systemConfigI18n.ts b/apps/dsa-web/src/utils/systemConfigI18n.ts index d97975d66..c217156b3 100644 --- a/apps/dsa-web/src/utils/systemConfigI18n.ts +++ b/apps/dsa-web/src/utils/systemConfigI18n.ts @@ -226,7 +226,7 @@ const fieldDescriptionMap: Record = { BRAVE_API_KEYS: '用于新闻检索的 Brave Search 密钥,支持逗号分隔多个。', STOCK_INDEX_REMOTE_UPDATE_ENABLED: '控制是否从 GitHub main 远程刷新股票自动补全索引;失败会降级到本地缓存或内置索引,不影响主分析流程。', SEARXNG_BASE_URLS: 'SearXNG 自建实例地址(逗号分隔,无配额兜底,需在 settings.yml 启用 format: json)。', - SEARXNG_PUBLIC_INSTANCES_ENABLED: '当未配置 SearXNG 自建实例时,自动从 searx.space 获取公共实例并轮询使用;设为 false 可禁用该默认行为。', + SEARXNG_PUBLIC_INSTANCES_ENABLED: '默认关闭;设为 true 后,当未配置 SearXNG 自建实例时,会从 searx.space 获取公共实例并轮询使用。', MINIMAX_API_KEYS: '用于新闻检索的 MiniMax 密钥,支持逗号分隔多个(最低优先级)。', NEWS_STRATEGY_PROFILE: '新闻窗口档位:ultra_short=1天,short=3天,medium=7天,long=30天。', NEWS_MAX_AGE_DAYS: '新闻最大时效上限。实际窗口 = min(策略档位天数, NEWS_MAX_AGE_DAYS)。例如 ultra_short + 7 仍为 1 天。', diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6972d9962..1cd86b50a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - [改进] AIHubMix 注册与引流链接统一使用 inferera.com,改善中国大陆网络直连体验。 - [修复] 单股推送模式在未配置通知渠道时仍会落盘本地个股报告;CLI 启动分析若因空股票列表、个股结果全失败或本地报告保存失败而未生成报告,会显式返回失败并记录原因。 - [修复] 合并推送模式下即使个股汇总报告落盘失败,仍会先发送已有的合并通知;仅启用大盘复盘但最终未生成任何复盘内容时,分析任务会显式返回失败。 +- [修复] SearXNG 公共实例发现的默认值由启用改为关闭:公共实例普遍存在限流、下线或不返回 JSON 的情况,默认开启会让未配置搜索 key 的用户每次分析多耗 30~60 秒且新闻面最终为空。运行时默认值、配置模板、中英文档与工作流诊断同步调整;显式设为 true 的用户行为不变。 - [新功能] Agent 工具调用支持按类别(data/search/analysis/action/market)配置默认超时,并允许单工具声明 `timeout_seconds`;有效超时按 first-wins 优先级解析(显式 per-run `tool_call_timeout_seconds` > 单工具显式 `timeout_seconds` > 类别默认 > 无限制),剩余 wall-clock 预算仅作不可突破的外层 cap,超时后返回结构化 `{"timeout": true}` 错误(标记 `retriable: false` 并写入 `non_retriable_tool_results` 防重试重复执行)供 Agent 继续执行而非中断循环(fixes #1890)。 - [修复] Agent 工具注册表(`src/agent/factory.get_tool_registry`)由模块级缓存改为按「类别超时映射的值」比对失效,规避 CPython 回收对象后地址复用(`id(config)` 相同)导致配置 reload 后的 `Config` 被误判为未变、沿用过期超时的真 bug;新增 `_coerce_config_timeout` 类型白名单,使调用方传入 `MagicMock` / 缺属性 stub / 脏字符串(如 `float(MagicMock())` 静默得到 1.0)时降级为「无类别限制」而非崩溃或强加 1 秒超时;`build_agent_executor(config)` / `build_agent_chat_executor(config)` 现已把调用方 `config` 透传给 `get_tool_registry(config)`(不再无参调用冻结首构 registry);`main._reload_runtime_config` 与 `SystemConfigService._reload_runtime_singletons`(及 `update()`→`reload_now` 路径)在配置热重载时调用 `reset_tool_registry()` 强制重建;回归测试补充「传入新 config 后 registry 重建」「reload 后新超时应生效」及「builder 透传 config」三类场景(#1890 的 review follow-up,闭环 OR-COM-dd1e8fa7 / OR-COM-bff42110) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index b4d508c78..b67af3a4a 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -434,8 +434,8 @@ git push -u origin main | `BOCHA_API_KEYS` | 博查搜索 API Key | 可选 | | `BRAVE_API_KEYS` | Brave Search API Key | 可选 | | `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search | 可选 | -| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时默认自动发现公共实例 | 可选 | -| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `true`) | 可选 | +| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时仅在显式启用公共实例发现后使用 `searx.space` | 可选 | +| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `false`) | 可选 | | `TUSHARE_TOKEN` | Tushare Token | 可选 | | `GEMINI_MODEL` | 模型名称(默认 gemini-2.0-flash) | 可选 | diff --git a/docs/docker/zeabur-deployment.md b/docs/docker/zeabur-deployment.md index 62dcc7c94..b93111ddb 100644 --- a/docs/docker/zeabur-deployment.md +++ b/docs/docker/zeabur-deployment.md @@ -163,8 +163,8 @@ Zeabur 服务建议从 `1G` 内存起步;`512M` 仅适合轻量 Web/API、单 | `BOCHA_API_KEYS` | Bocha API 密钥(用逗号分隔) | | `BRAVE_API_KEYS` | Brave Search API 密钥(用逗号分隔) | | `MINIMAX_API_KEYS` | MiniMax API 密钥(用逗号分隔) | -| `SEARXNG_BASE_URLS` | SearXNG 实例地址(逗号分隔,无配额兜底,需在 settings.yml 启用 format: json);留空时默认自动发现公共实例 | -| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `true`) | +| `SEARXNG_BASE_URLS` | SearXNG 实例地址(逗号分隔,无配额兜底,需在 settings.yml 启用 format: json);留空时仅在显式启用公共实例发现后使用 `searx.space` | +| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `false`) | ### 5.4 配置方法 diff --git a/docs/full-guide.md b/docs/full-guide.md index e0f879f52..9d8c45fd9 100644 --- a/docs/full-guide.md +++ b/docs/full-guide.md @@ -163,8 +163,8 @@ daily_stock_analysis/ | `BOCHA_API_KEYS` | [博查搜索](https://open.bocha.cn/) Web Search API(中文搜索优化,支持AI摘要,多个key用逗号分隔) | 可选 | | `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API(隐私优先,美股优化,多个key用逗号分隔) | 可选 | | `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimax.io/) Coding Plan Web Search(结构化搜索结果) | 可选 | -| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时默认自动发现公共实例 | 可选 | -| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `true`) | 可选 | +| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时仅在显式启用公共实例发现后使用 `searx.space` | 可选 | +| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `false`)。公共实例普遍限流或未开启 JSON 输出,开启后每次分析可能多耗 30~60 秒且新闻面为空 | 可选 | | `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可选 | | `TUSHARE_HTTP_URL` | Tushare Pro HTTP 接入地址;留空(或未设置/空白)时使用官方端点 `http://api.tushare.pro`,仅在需通过公司内网代理、跨境网络或自建镜像时填写 `http://` 或 `https://` 开头的完整地址 | 可选 | | `TICKFLOW_API_KEY` | [TickFlow](https://tickflow.org) API Key;可选,用于 A 股日 K、实时行情、股票列表/名称与大盘复盘增强;失败或权限不足时自动回退。 | 可选 | @@ -373,8 +373,8 @@ daily_stock_analysis/ | `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search(结构化搜索结果) | 可选 | | `SOCIAL_SENTIMENT_API_KEY` | Stock Sentiment API Key(Reddit / X / Polymarket,可选) | 可选 | | `SOCIAL_SENTIMENT_API_URL` | Stock Sentiment API 地址(默认 `https://api.adanos.org`) | 可选 | -| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时默认自动发现公共实例 | 可选 | -| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `true`) | 可选 | +| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时仅在显式启用公共实例发现后使用 `searx.space` | 可选 | +| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `false`)。公共实例普遍限流或未开启 JSON 输出,开启后每次分析可能多耗 30~60 秒且新闻面为空 | 可选 | | `NEWS_STRATEGY_PROFILE` | 新闻策略窗口档位:`ultra_short`(1天)/`short`(3天)/`medium`(7天)/`long`(30天);实际窗口取与 `NEWS_MAX_AGE_DAYS` 的最小值 | 默认 `short` | | `NEWS_MAX_AGE_DAYS` | 新闻最大时效(天),搜索时限制结果在近期内 | 默认 `3` | | `BIAS_THRESHOLD` | 乖离率阈值(%),超过提示不追高;强势趋势股自动放宽到 1.5 倍 | 默认 `5.0` | diff --git a/docs/full-guide_EN.md b/docs/full-guide_EN.md index 6f28c6f45..a0564813e 100644 --- a/docs/full-guide_EN.md +++ b/docs/full-guide_EN.md @@ -156,8 +156,8 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions` | `BOCHA_API_KEYS` | [Bocha Search](https://open.bocha.cn/) Web Search API (Chinese search optimized, supports AI summaries, multiple keys comma-separated) | Optional | | `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API (privacy-first, US-stock news enrichment, comma-separated for multiple keys) | Optional | | `MINIMAX_API_KEYS` | [MiniMax](https://platform.minimax.io/) Coding Plan Web Search (structured search results) | Optional | -| `SEARXNG_BASE_URLS` | SearXNG self-hosted instances (quota-free fallback, enable format: json in settings.yml); when empty the app auto-discovers public instances | Optional | -| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | Auto-discover public SearXNG instances from `searx.space` when `SEARXNG_BASE_URLS` is empty (default `true`) | Optional | +| `SEARXNG_BASE_URLS` | SearXNG self-hosted instances (quota-free fallback, enable format: json in settings.yml); when empty, `searx.space` discovery is used only if public instances are explicitly enabled | Optional | +| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | Auto-discover public SearXNG instances from `searx.space` when `SEARXNG_BASE_URLS` is empty (default `false`). Public instances are commonly rate-limited or do not return JSON, so enabling this can add 30-60s per run and still yield no news | Optional | | `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638) Token | Optional | | `TUSHARE_HTTP_URL` | Tushare Pro HTTP endpoint; when unset/empty defaults to the official `http://api.tushare.pro`. Set to a `http://` or `https://` URL only when routing through a corporate proxy, cross-border network, or a self-hosted mirror | Optional | | `TICKFLOW_API_KEY` | [TickFlow](https://tickflow.org) API key for optional A-share daily K-lines, realtime quotes, stock list/name lookup, and CN market review enhancement; permission or entitlement failures fall back to existing providers | Optional | @@ -336,8 +336,8 @@ For the notification baseline, diagnostics, and deployment notes, see [Notificat | `MINIMAX_API_KEYS` | MiniMax Coding Plan Web Search (structured results) | Optional | | `SOCIAL_SENTIMENT_API_KEY` | Stock Sentiment API Key (Reddit / X / Polymarket, US stocks optional) | Optional | | `SOCIAL_SENTIMENT_API_URL` | Stock Sentiment API endpoint (default `https://api.adanos.org`) | Optional | -| `SEARXNG_BASE_URLS` | SearXNG self-hosted instances (quota-free fallback, enable format: json in settings.yml); when empty the app auto-discovers public instances | Optional | -| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | Auto-discover public SearXNG instances from `searx.space` when `SEARXNG_BASE_URLS` is empty (default `true`) | Optional | +| `SEARXNG_BASE_URLS` | SearXNG self-hosted instances (quota-free fallback, enable format: json in settings.yml); when empty, `searx.space` discovery is used only if public instances are explicitly enabled | Optional | +| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | Auto-discover public SearXNG instances from `searx.space` when `SEARXNG_BASE_URLS` is empty (default `false`). Public instances are commonly rate-limited or do not return JSON, so enabling this can add 30-60s per run and still yield no news | Optional | > Behavior note: Search and social sentiment are optional enhancement services. If either service fails to initialize, the system logs a warning and degrades gracefully by skipping that stage without blocking the core analysis flow. diff --git a/src/config.py b/src/config.py index 657c6d9f3..a9cf21de0 100644 --- a/src/config.py +++ b/src/config.py @@ -979,7 +979,7 @@ class Config: brave_api_keys: List[str] = field(default_factory=list) # Brave Search API Keys 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 = True # Auto-discover public SearXNG instances when base URLs are absent + searxng_public_instances_enabled: bool = False # Opt in to public discovery when base URLs are absent # === Social Sentiment (US stocks only, api.adanos.org) === social_sentiment_api_key: Optional[str] = None @@ -1714,7 +1714,7 @@ class Config: ) searxng_public_instances_enabled = parse_env_bool( os.getenv('SEARXNG_PUBLIC_INSTANCES_ENABLED'), - default=True, + default=False, ) # 企微消息类型与最大字节数逻辑 diff --git a/src/core/config_registry.py b/src/core/config_registry.py index c9f880f0c..ff5c50823 100644 --- a/src/core/config_registry.py +++ b/src/core/config_registry.py @@ -1102,14 +1102,14 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = { }, "SEARXNG_PUBLIC_INSTANCES_ENABLED": { "title": "SearXNG Public Instances", - "description": "Auto-discover public SearXNG instances from searx.space when SEARXNG_BASE_URLS is empty. Default: true; set false to disable.", + "description": "Auto-discover public SearXNG instances from searx.space when SEARXNG_BASE_URLS is empty. Default: false; set true to enable.", "category": "data_source", "data_type": "boolean", "ui_control": "switch", "is_sensitive": False, "is_required": False, "is_editable": True, - "default_value": "true", + "default_value": "false", "options": [], "validation": {}, "display_order": 53, @@ -4659,8 +4659,8 @@ _FIELD_HELP_METADATA: Dict[str, Dict[str, Any]] = { "SEARXNG_PUBLIC_INSTANCES_ENABLED": { "help_key": "settings.data_source.SEARXNG_BASE_URLS", "examples": [ - "SEARXNG_PUBLIC_INSTANCES_ENABLED=true", "SEARXNG_PUBLIC_INSTANCES_ENABLED=false", + "SEARXNG_PUBLIC_INSTANCES_ENABLED=true", ], "docs": _DOC_FULL_GUIDE_SEARCH, "warning_codes": ["public_instance_stability"], diff --git a/src/core/market_review_runtime.py b/src/core/market_review_runtime.py index d0fb93fe5..53378bc34 100644 --- a/src/core/market_review_runtime.py +++ b/src/core/market_review_runtime.py @@ -103,7 +103,7 @@ def build_market_review_runtime( searxng_public_instances_enabled=getattr( config, "searxng_public_instances_enabled", - True, + False, ), news_max_age_days=getattr(config, "news_max_age_days", 3), news_strategy_profile=getattr(config, "news_strategy_profile", "short"), diff --git a/src/search_service.py b/src/search_service.py index 83b4bd2d1..3931bcf7c 100644 --- a/src/search_service.py +++ b/src/search_service.py @@ -2389,7 +2389,7 @@ class SearchService: serpapi_keys: Optional[List[str]] = None, minimax_keys: Optional[List[str]] = None, searxng_base_urls: Optional[List[str]] = None, - searxng_public_instances_enabled: bool = True, + searxng_public_instances_enabled: bool = False, news_max_age_days: int = 3, news_strategy_profile: str = "short", ): diff --git a/tests/test_market_review_runtime.py b/tests/test_market_review_runtime.py index 52c782300..60d3ecae6 100644 --- a/tests/test_market_review_runtime.py +++ b/tests/test_market_review_runtime.py @@ -78,6 +78,24 @@ class TestMarketReviewRuntimeCompatibility(unittest.TestCase): self.assertIs(runtime_analyzer, analyzer) self.assertIsNone(runtime_search) + def test_build_market_review_runtime_defaults_legacy_public_discovery_off(self) -> None: + config = self._base_config() + del config.searxng_public_instances_enabled + config.has_search_capability_enabled = lambda: True + notifier = MagicMock() + search_service = MagicMock() + + with patch("src.notification.NotificationService", return_value=notifier), \ + patch("src.search_service.SearchService", return_value=search_service) as search_cls: + runtime_notifier, runtime_analyzer, runtime_search = build_market_review_runtime(config) + + self.assertFalse( + search_cls.call_args.kwargs["searxng_public_instances_enabled"] + ) + self.assertIs(runtime_notifier, notifier) + self.assertIsNone(runtime_analyzer) + self.assertIs(runtime_search, search_service) + def test_build_market_review_runtime_supports_litellm_channel_model_list(self) -> None: config = self._base_config() config.litellm_model = "" diff --git a/tests/test_search_searxng.py b/tests/test_search_searxng.py index 1bccc96e2..3e4eb87fc 100644 --- a/tests/test_search_searxng.py +++ b/tests/test_search_searxng.py @@ -441,6 +441,13 @@ class TestSearXNGSearchProvider(unittest.TestCase): self.assertTrue(service.is_available) self.assertTrue(any(provider.name == "SearXNG" for provider in service._providers)) + def test_search_service_does_not_add_public_searxng_provider_by_default(self): + service = SearchService() + + self.assertFalse(service.is_available) + self.assertFalse(any(provider.name == "SearXNG" for provider in service._providers)) + self.assertFalse(service._constructor_kwargs["searxng_public_instances_enabled"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_searxng_env_docs.py b/tests/test_searxng_env_docs.py index bec1cc0de..4ed8dbbfc 100644 --- a/tests/test_searxng_env_docs.py +++ b/tests/test_searxng_env_docs.py @@ -8,7 +8,7 @@ def test_env_example_documents_searxng_actions_variable_mapping() -> None: env_example = (ROOT_DIR / ".env.example").read_text(encoding="utf-8") start = env_example.index("# SearXNG 实例地址") - end = env_example.index("SEARXNG_PUBLIC_INSTANCES_ENABLED=true", start) + end = env_example.index("SEARXNG_PUBLIC_INSTANCES_ENABLED=", start) searxng_block = env_example[start:end] assert "GitHub Actions" in searxng_block @@ -17,6 +17,15 @@ def test_env_example_documents_searxng_actions_variable_mapping() -> None: assert "需配置为 Secret" not in searxng_block +def test_env_example_defaults_public_searxng_instances_off() -> None: + """公共实例发现默认关闭:公共实例普遍限流或不返回 JSON, + 默认开启会让未配置搜索 key 的用户每次分析白等 30~60 秒且拿到空新闻面。""" + env_example = (ROOT_DIR / ".env.example").read_text(encoding="utf-8") + + assert "SEARXNG_PUBLIC_INSTANCES_ENABLED=false" in env_example + assert "SEARXNG_PUBLIC_INSTANCES_ENABLED=true" not in env_example + + def test_daily_analysis_workflow_matches_documented_searxng_variable_mapping() -> None: workflow = ( ROOT_DIR / ".github" / "workflows" / "00-daily-analysis.yml" @@ -36,3 +45,89 @@ def test_changelog_mentions_searxng_actions_variable_mapping() -> None: "- [修复] GitHub Actions 每日分析工作流读取 SearXNG 自建实例地址时" "支持 Variables 优先、Secrets 回退,修复仅配置 Variables 时 URL 不生效的问题。" ) in changelog + + +def test_runtime_default_disables_public_searxng_instances(monkeypatch) -> None: + """未设置环境变量时,运行时必须解析为关闭。 + + 这是本项改动真正生效的地方:只改 .env.example 只影响复制了新模板的用户, + 已有安装和 GitHub Actions(未配置 Variable/Secret 时变量为空)仍会 + 走公共实例发现,继续承受 30~60 秒的失败重试。 + """ + from src.config import parse_env_bool + + monkeypatch.delenv("SEARXNG_PUBLIC_INSTANCES_ENABLED", raising=False) + import os + + assert ( + parse_env_bool(os.getenv("SEARXNG_PUBLIC_INSTANCES_ENABLED"), default=False) + is False + ) + + +def test_config_constructor_defaults_public_searxng_instances_off() -> None: + """直接构造 Config 时也必须遵循公共实例发现默认关闭的契约。""" + from src.config import Config + + assert Config().searxng_public_instances_enabled is False + + +def test_config_module_uses_false_as_runtime_default() -> None: + """锁住 config.py 里的默认值,防止后续改动悄悄回退。""" + src = (ROOT_DIR / "src" / "config.py").read_text(encoding="utf-8") + idx = src.index("SEARXNG_PUBLIC_INSTANCES_ENABLED") + window = src[idx : idx + 200] + + assert "default=False" in window + assert "default=True" not in window + + +def test_workflow_reports_disabled_when_variable_unset() -> None: + """工作流诊断不得把「未设置」显示成已开启,否则与运行时行为不一致。""" + workflow = ( + ROOT_DIR / ".github" / "workflows" / "00-daily-analysis.yml" + ).read_text(encoding="utf-8") + + assert "默认开启" not in workflow + assert "默认关闭" in workflow + + +def test_docs_describe_public_searxng_discovery_as_opt_in() -> None: + chinese_docs = [ + ROOT_DIR / "docs" / "full-guide.md", + ROOT_DIR / "docs" / "DEPLOY.md", + ROOT_DIR / "docs" / "docker" / "zeabur-deployment.md", + ] + for path in chinese_docs: + content = path.read_text(encoding="utf-8") + base_url_lines = [ + line for line in content.splitlines() if "`SEARXNG_BASE_URLS`" in line + ] + public_toggle_lines = [ + line + for line in content.splitlines() + if "`SEARXNG_PUBLIC_INSTANCES_ENABLED`" in line + ] + assert base_url_lines + assert public_toggle_lines + assert all("留空时默认自动发现公共实例" not in line for line in base_url_lines) + assert all("默认 `false`" in line for line in public_toggle_lines) + + english_guide = (ROOT_DIR / "docs" / "full-guide_EN.md").read_text( + encoding="utf-8" + ) + english_base_url_lines = [ + line for line in english_guide.splitlines() if "`SEARXNG_BASE_URLS`" in line + ] + english_public_toggle_lines = [ + line + for line in english_guide.splitlines() + if "`SEARXNG_PUBLIC_INSTANCES_ENABLED`" in line + ] + assert english_base_url_lines + assert english_public_toggle_lines + assert all( + "when empty the app auto-discovers public instances" not in line + for line in english_base_url_lines + ) + assert all("default `false`" in line for line in english_public_toggle_lines) diff --git a/tests/test_system_config_service.py b/tests/test_system_config_service.py index cfb784e42..f3b87a169 100644 --- a/tests/test_system_config_service.py +++ b/tests/test_system_config_service.py @@ -714,6 +714,16 @@ class SystemConfigServiceTestCase(unittest.TestCase): self.assertEqual(items["REPORT_SHOW_LLM_MODEL"]["value"], "false") self.assertTrue(items["REPORT_SHOW_LLM_MODEL"]["raw_value_exists"]) + def test_get_config_defaults_public_searxng_instances_off(self) -> None: + payload = self.service.get_config(include_schema=True) + items = {item["key"]: item for item in payload["items"]} + public_instances = items["SEARXNG_PUBLIC_INSTANCES_ENABLED"] + + self.assertEqual(public_instances["value"], "false") + self.assertFalse(public_instances["raw_value_exists"]) + self.assertEqual(public_instances["schema"]["default_value"], "false") + self.assertIn("Default: false", public_instances["schema"]["description"]) + def test_get_config_preserves_manual_agent_codex_cli_value_without_schema_option(self) -> None: for backend in sorted(GENERATION_ONLY_BACKEND_IDS): with self.subTest(backend=backend):