fix: 公共 SearXNG 实例发现默认关闭 (#2226)

* 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 <zz-b240@zz-b240deMacBook-Air.local>
Co-authored-by: ZhuLinsen <zhuls97@163.com>
This commit is contained in:
青玉案
2026-08-19 21:59:10 +08:00
committed by GitHub
parent cfd6b0a5fb
commit b740beea2a
17 changed files with 167 additions and 24 deletions

View File

@@ -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)

View File

@@ -497,7 +497,7 @@ jobs:
echo " SearXNG Public Instances: ❌ 已禁用"
;;
"")
echo " SearXNG Public Instances: 默认开启"
echo " SearXNG Public Instances: 默认关闭(公共实例普遍限流,如需启用请显式设为 true"
;;
*)
echo " SearXNG Public Instances: ✅ 已启用"

View File

@@ -11,6 +11,16 @@ const flattenHelp = (help: ReturnType<typeof getSettingsHelpContent>) => [
...(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(

View File

@@ -226,7 +226,7 @@ const fieldDescriptionMap: Record<string, string> = {
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 天。',

View File

@@ -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

View File

@@ -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 | 可选 |

View File

@@ -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 配置方法

View File

@@ -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 KeyReddit / 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` |

View File

@@ -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.

View File

@@ -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,
)
# 企微消息类型与最大字节数逻辑

View File

@@ -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"],

View File

@@ -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"),

View File

@@ -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",
):

View File

@@ -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 = ""

View File

@@ -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()

View File

@@ -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)

View File

@@ -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):