argo-search 1.0.1 → 2.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/README.en.md +735 -0
- package/README.es.md +777 -0
- package/README.ja.md +777 -0
- package/README.ko.md +777 -0
- package/README.md +778 -320
- package/SKILL.md +135 -0
- package/assets/readme/hero.svg +121 -0
- package/assets/readme/made-with-beautify.svg +10 -0
- package/assets/readme/proof-routes.svg +73 -0
- package/assets/readme/why-better.svg +104 -0
- package/assets/readme/workflow.svg +83 -0
- package/backends/domain_profiles.json +2356 -96
- package/backends/engine_registry.yaml +3428 -503
- package/backends/query_synonyms_cn.json +35 -0
- package/backends/quota_profiles.json +2488 -160
- package/backends/source_types_cn.json +109 -0
- package/backends/text_lexicon.json +44 -0
- package/bin/argo +772 -0
- package/bin/argo.js +102 -39
- package/config.yaml +5139 -367
- package/cordis.patch.yml +21 -0
- package/data/countries.json +40 -0
- package/data/lang_matrix/lang_capability.json +842 -0
- package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
- package/data/regions_cn.json +1 -0
- package/docs/ADDING_NEW_ENGINE.md +184 -0
- package/docs/ARGO_INTRO.md +124 -0
- package/docs/DATA_SOURCE_GAPS.md +81 -0
- package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
- package/docs/ENGINE_CATALOG.md +575 -0
- package/docs/MCP_SETUP.md +108 -0
- package/docs/PLATFORM_PATHS.md +111 -0
- package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
- package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
- package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
- package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
- package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
- package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
- package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
- package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
- package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
- package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
- package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
- package/docs/SEARCH_ARCHIVE.md +115 -0
- package/docs/assets/architecture.svg +104 -0
- package/docs/assets/hero.svg +98 -0
- package/docs/engines/bangumi.md +27 -0
- package/docs/engines/biorxiv.md +27 -0
- package/docs/engines/clawhub.md +27 -0
- package/docs/engines/crt_sh.md +27 -0
- package/docs/engines/datacite.md +27 -0
- package/docs/engines/deps_dev.md +27 -0
- package/docs/engines/douban_movie.md +27 -0
- package/docs/engines/endoflife.md +27 -0
- package/docs/engines/flk_law.md +27 -0
- package/docs/engines/fxtwitter.md +27 -0
- package/docs/engines/google_news.md +27 -0
- package/docs/engines/gov_regulations.md +27 -0
- package/docs/engines/guqian.md +27 -0
- package/docs/engines/hackernews.md +27 -0
- package/docs/engines/iplant.md +27 -0
- package/docs/engines/keenable.md +27 -0
- package/docs/engines/met_museum.md +27 -0
- package/docs/engines/nasa_images.md +27 -0
- package/docs/engines/netease_music.md +27 -0
- package/docs/engines/npm.md +27 -0
- package/docs/engines/nvd.md +27 -0
- package/docs/engines/openreview.md +27 -0
- package/docs/engines/openstd.md +27 -0
- package/docs/engines/parallel_free.md +27 -0
- package/docs/engines/people_daily.md +27 -0
- package/docs/engines/realtime_index.md +27 -0
- package/docs/engines/sec_edgar.md +27 -0
- package/docs/engines/seltz.md +27 -0
- package/docs/engines/skillsmp.md +27 -0
- package/docs/engines/sspai.md +27 -0
- package/docs/engines/std_samr.md +27 -0
- package/docs/engines/train.md +27 -0
- package/docs/engines/un_comtrade.md +27 -0
- package/docs/engines/wikisource.md +27 -0
- package/docs/engines/zdic.md +27 -0
- package/docs/engines/zenodo.md +27 -0
- package/docs/engines/zhihu_hot_app.md +27 -0
- package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
- package/engines/README.md +10 -0
- package/engines/_template_cli.yaml +39 -0
- package/engines/_template_http.yaml +37 -0
- package/engines/plugins/_template_plugin.py +50 -0
- package/engines/specs/_cisa_kev.yaml +54 -0
- package/engines/specs/_osv.yaml +63 -0
- package/engines/specs/aljazeera.yaml +43 -0
- package/engines/specs/antara.yaml +43 -0
- package/engines/specs/artic.yaml +20 -0
- package/engines/specs/aviation_weather.yaml +39 -0
- package/engines/specs/bangumi.yaml +21 -0
- package/engines/specs/biorxiv.yaml +24 -0
- package/engines/specs/carbon_intensity.yaml +32 -0
- package/engines/specs/cfets.yaml +41 -0
- package/engines/specs/cinii.yaml +44 -0
- package/engines/specs/clawhub.yaml +49 -0
- package/engines/specs/cleveland.yaml +19 -0
- package/engines/specs/cn_ai_news.yaml +43 -0
- package/engines/specs/core.yaml +35 -0
- package/engines/specs/crt_sh.yaml +38 -0
- package/engines/specs/ct24.yaml +43 -0
- package/engines/specs/datacite.yaml +27 -0
- package/engines/specs/datagov_uk.yaml +41 -0
- package/engines/specs/ddgs_images.yaml +45 -0
- package/engines/specs/deepseek_search.yaml +74 -0
- package/engines/specs/deezer.yaml +20 -0
- package/engines/specs/defillama.yaml +38 -0
- package/engines/specs/deps_dev.yaml +21 -0
- package/engines/specs/digup.yaml +36 -0
- package/engines/specs/doi.yaml +39 -0
- package/engines/specs/douban_movie.yaml +22 -0
- package/engines/specs/dw_news.yaml +47 -0
- package/engines/specs/egov_law.yaml +21 -0
- package/engines/specs/elpais.yaml +43 -0
- package/engines/specs/endoflife.yaml +21 -0
- package/engines/specs/energy_charts.yaml +21 -0
- package/engines/specs/factcheck_org.yaml +52 -0
- package/engines/specs/faz.yaml +43 -0
- package/engines/specs/federal_register.yaml +57 -0
- package/engines/specs/firecrawl.yaml +53 -0
- package/engines/specs/flk_law.yaml +24 -0
- package/engines/specs/folha.yaml +43 -0
- package/engines/specs/france24.yaml +44 -0
- package/engines/specs/full_fact.yaml +47 -0
- package/engines/specs/fxtwitter.yaml +36 -0
- package/engines/specs/gbfs_nyc.yaml +32 -0
- package/engines/specs/gdacs.yaml +21 -0
- package/engines/specs/gdelt.yaml +64 -0
- package/engines/specs/gleif.yaml +55 -0
- package/engines/specs/google_news.yaml +22 -0
- package/engines/specs/gov_regulations.yaml +38 -0
- package/engines/specs/guardian_rss.yaml +59 -0
- package/engines/specs/hkex.yaml +40 -0
- package/engines/specs/hurriyet.yaml +43 -0
- package/engines/specs/iplant.yaml +25 -0
- package/engines/specs/jikan.yaml +24 -0
- package/engines/specs/jolpica.yaml +20 -0
- package/engines/specs/k10plus.yaml +20 -0
- package/engines/specs/keenable.yaml +44 -0
- package/engines/specs/keenable_free.yaml +36 -0
- package/engines/specs/lefigaro.yaml +43 -0
- package/engines/specs/listenbrainz.yaml +19 -0
- package/engines/specs/matichon.yaml +43 -0
- package/engines/specs/met_museum.yaml +21 -0
- package/engines/specs/nasa_images.yaml +45 -0
- package/engines/specs/netease_music.yaml +38 -0
- package/engines/specs/nhk.yaml +43 -0
- package/engines/specs/nhtsa_vpic.yaml +20 -0
- package/engines/specs/noaa_swpc.yaml +20 -0
- package/engines/specs/nvd.yaml +38 -0
- package/engines/specs/obis.yaml +20 -0
- package/engines/specs/opencitations.yaml +52 -0
- package/engines/specs/openf1.yaml +21 -0
- package/engines/specs/openfoodfacts.yaml +45 -0
- package/engines/specs/openligadb.yaml +20 -0
- package/engines/specs/openreview.yaml +37 -0
- package/engines/specs/openstd.yaml +21 -0
- package/engines/specs/people_daily.yaml +22 -0
- package/engines/specs/realtime_index.yaml +34 -0
- package/engines/specs/ror.yaml +20 -0
- package/engines/specs/satnogs.yaml +20 -0
- package/engines/specs/sec_edgar.yaml +46 -0
- package/engines/specs/shfe.yaml +39 -0
- package/engines/specs/skillsmp.yaml +50 -0
- package/engines/specs/sogou_weixin_browser.yaml +30 -0
- package/engines/specs/soilgrids.yaml +25 -0
- package/engines/specs/sspai.yaml +36 -0
- package/engines/specs/stackexchange.yaml +46 -0
- package/engines/specs/std_samr.yaml +21 -0
- package/engines/specs/tass.yaml +43 -0
- package/engines/specs/tavily_free.yaml +56 -0
- package/engines/specs/tle_mirror.yaml +21 -0
- package/engines/specs/train.yaml +37 -0
- package/engines/specs/tvmaze.yaml +20 -0
- package/engines/specs/twitter_syndication.yaml +31 -0
- package/engines/specs/un_comtrade.yaml +23 -0
- package/engines/specs/unpaywall.yaml +54 -0
- package/engines/specs/vnpress.yaml +43 -0
- package/engines/specs/weather.yaml +32 -0
- package/engines/specs/who_don.yaml +21 -0
- package/engines/specs/who_gho.yaml +20 -0
- package/engines/specs/wikimedia_commons.yaml +34 -0
- package/engines/specs/wikisource.yaml +22 -0
- package/engines/specs/worms.yaml +19 -0
- package/engines/specs/x_trending.yaml +34 -0
- package/engines/specs/yna.yaml +43 -0
- package/engines/specs/ynet.yaml +43 -0
- package/engines/specs/yuanbao.yaml +93 -0
- package/engines/specs/zbmath.yaml +40 -0
- package/engines/specs/zdic.yaml +22 -0
- package/engines/specs/zenodo.yaml +27 -0
- package/engines/specs/zhihu_hot_app.yaml +35 -0
- package/mcp/clients.yaml +106 -0
- package/package.json +55 -12
- package/packages/dsh-plugin/README.md +152 -0
- package/packages/dsh-plugin/cordis.patch.yml +48 -0
- package/packages/dsh-plugin/dsh/client.js +1533 -0
- package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
- package/packages/dsh-plugin/dsh/index.js +527 -0
- package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +38 -0
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +406 -0
- package/packages/dsh-plugin/test/config.test.js +30 -0
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +1382 -0
- package/references/query-templates.md +96 -0
- package/references/query-templates.yaml +118 -0
- package/scripts/ab_eval.py +219 -0
- package/scripts/ab_eval_p0p1.py +266 -0
- package/scripts/adaptive.py +294 -70
- package/scripts/answer.py +154 -0
- package/scripts/archive_run.py +591 -0
- package/scripts/argo_engine_registry.py +172 -0
- package/scripts/argo_paths.py +728 -0
- package/scripts/article.py +136 -0
- package/scripts/article_meta.py +131 -0
- package/scripts/arxiv_source.py +201 -0
- package/scripts/badcase_ledger.py +252 -0
- package/scripts/batch_probe.py +241 -0
- package/scripts/benchmark.py +19 -0
- package/scripts/bounded_run.py +144 -0
- package/scripts/browser_auth.py +323 -0
- package/scripts/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/burstiness_detector.py +147 -0
- package/scripts/cache.py +1066 -67
- package/scripts/cache_guard.py +271 -0
- package/scripts/cache_key_vdom.py +56 -0
- package/scripts/cache_similarity.py +170 -0
- package/scripts/caliber_isolation.py +195 -0
- package/scripts/calibrate_lowquality.py +210 -0
- package/scripts/candidate_envelope.py +366 -0
- package/scripts/chrome_cdp.py +871 -0
- package/scripts/ci_drift_gate.py +90 -0
- package/scripts/circuit_breaker.py +389 -0
- package/scripts/citations.py +334 -0
- package/scripts/cite_cli.py +59 -0
- package/scripts/cjk_tokens.py +95 -0
- package/scripts/clarify.py +47 -24
- package/scripts/cli_io.py +240 -0
- package/scripts/config.py +683 -45
- package/scripts/config_cache.py +402 -0
- package/scripts/conn_pool.py +220 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_security.py +685 -0
- package/scripts/content_signals.py +751 -0
- package/scripts/crawl.py +74 -35
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/daemon.py +866 -0
- package/scripts/digup_bridge.py +214 -0
- package/scripts/dispatch_result.py +98 -0
- package/scripts/engine_admission.py +366 -0
- package/scripts/engine_dispatch.py +982 -0
- package/scripts/engine_env.py +539 -0
- package/scripts/engine_failure.py +411 -0
- package/scripts/engine_families.py +738 -0
- package/scripts/engine_policy.py +218 -0
- package/scripts/engine_requires.py +190 -0
- package/scripts/engine_status.py +322 -0
- package/scripts/engine_validate.py +638 -0
- package/scripts/engines.py +799 -294
- package/scripts/engines_base.py +1521 -0
- package/scripts/engines_builders.py +310 -0
- package/scripts/engines_builders_batch10.py +204 -0
- package/scripts/engines_builders_batch11.py +174 -0
- package/scripts/engines_builders_batch12.py +103 -0
- package/scripts/engines_builders_batch13.py +340 -0
- package/scripts/engines_builders_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -0
- package/scripts/engines_builders_batch9.py +1468 -0
- package/scripts/engines_builders_cn.py +2283 -0
- package/scripts/engines_builders_data.py +2923 -0
- package/scripts/engines_builders_data_macro.py +775 -0
- package/scripts/engines_builders_feeds.py +255 -0
- package/scripts/engines_builders_intl.py +1239 -0
- package/scripts/engines_builders_keyless.py +111 -0
- package/scripts/engines_builders_search.py +440 -0
- package/scripts/engines_builders_tech.py +1359 -0
- package/scripts/evidence.py +620 -166
- package/scripts/evidence_loop.py +624 -0
- package/scripts/evidence_tier.py +298 -0
- package/scripts/except_sets.py +25 -0
- package/scripts/extract.py +55 -16
- package/scripts/fact_align.py +207 -0
- package/scripts/fact_crosscheck.py +230 -0
- package/scripts/fetch.py +25 -12
- package/scripts/fetch_browser.py +87 -0
- package/scripts/fetch_jsrun.py +122 -0
- package/scripts/fetch_quality.py +225 -0
- package/scripts/fetch_render_tinyfish.py +117 -0
- package/scripts/fetch_v3.py +1804 -0
- package/scripts/focus_extract.py +350 -0
- package/scripts/fulltext_store.py +169 -0
- package/scripts/fxtwitter_api.py +325 -0
- package/scripts/gen_engine_catalog.py +364 -0
- package/scripts/gen_native_tools.py +91 -0
- package/scripts/gen_routing_snapshot.py +195 -0
- package/scripts/health_check.py +140 -402
- package/scripts/health_probe.py +23 -8
- package/scripts/hot_state.py +134 -0
- package/scripts/http_client.py +1053 -0
- package/scripts/image_ops.py +430 -0
- package/scripts/image_vision/vision_probe.swift +126 -0
- package/scripts/install.ps1 +166 -0
- package/scripts/install.sh +221 -0
- package/scripts/job.py +1186 -0
- package/scripts/jump_resolver.py +235 -0
- package/scripts/lang_capability.py +150 -0
- package/scripts/lang_detect.py +289 -0
- package/scripts/lang_matrix_probe.py +329 -0
- package/scripts/lang_pref.py +311 -0
- package/scripts/link_source.py +340 -0
- package/scripts/local_image.py +970 -0
- package/scripts/local_seek.py +402 -0
- package/scripts/macro_countries.py +97 -0
- package/scripts/matrix_search_eval.py +1187 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_diag.py +8 -3
- package/scripts/mcp_handlers.py +492 -0
- package/scripts/mcp_handlers_surface.py +157 -0
- package/scripts/mcp_launch.sh +124 -0
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +125 -419
- package/scripts/mcp_setup.py +431 -0
- package/scripts/mcp_tools.py +331 -0
- package/scripts/mcp_transport.py +191 -0
- package/scripts/mcp_warm.py +127 -0
- package/scripts/min_version_scan.py +335 -0
- package/scripts/net_proxy.py +258 -0
- package/scripts/network_aware.py +152 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/paper.py +274 -0
- package/scripts/pdf_extract.py +518 -0
- package/scripts/phrase_filter.py +66 -0
- package/scripts/plan.py +422 -0
- package/scripts/postprocess_signals.py +150 -0
- package/scripts/pre-commit +48 -0
- package/scripts/query_enhance.py +157 -0
- package/scripts/query_operators.py +196 -0
- package/scripts/query_rewriter.py +116 -7
- package/scripts/query_signals.py +300 -0
- package/scripts/query_templates.py +167 -0
- package/scripts/query_understanding.py +411 -0
- package/scripts/query_variants.py +298 -0
- package/scripts/quota.py +633 -76
- package/scripts/rank_signals.py +775 -0
- package/scripts/ranking_eval.py +319 -0
- package/scripts/readability_extract.py +433 -0
- package/scripts/recompute.py +415 -0
- package/scripts/recovery.py +616 -0
- package/scripts/recovery_l0.py +160 -0
- package/scripts/recovery_runner.py +150 -0
- package/scripts/redskill/redskill_engine.py +332 -0
- package/scripts/regression_p0p1.py +318 -0
- package/scripts/replay_eval.py +658 -0
- package/scripts/rerank_tfidf.py +225 -0
- package/scripts/research.py +607 -403
- package/scripts/research_cli.py +361 -0
- package/scripts/research_dossier.py +492 -0
- package/scripts/research_expand.py +169 -0
- package/scripts/research_gates.py +272 -0
- package/scripts/research_report.py +209 -0
- package/scripts/research_strategy.py +37 -0
- package/scripts/research_work_packages.py +195 -0
- package/scripts/result_lang.py +311 -0
- package/scripts/retry_policy.py +76 -0
- package/scripts/robots_guard.py +259 -0
- package/scripts/route.py +921 -254
- package/scripts/route_bangs.py +40 -0
- package/scripts/route_cache.py +243 -0
- package/scripts/route_combo.py +917 -0
- package/scripts/route_domains.py +341 -0
- package/scripts/route_lang.py +605 -0
- package/scripts/route_log.py +63 -0
- package/scripts/route_policy.py +250 -0
- package/scripts/rrf_classic.py +66 -0
- package/scripts/screenshot.py +73 -0
- package/scripts/search.py +886 -406
- package/scripts/search_attribution.py +185 -0
- package/scripts/search_benchmark.py +407 -0
- package/scripts/search_cli.py +640 -0
- package/scripts/search_entry.py +395 -0
- package/scripts/search_output.py +690 -0
- package/scripts/search_pipeline.py +926 -0
- package/scripts/search_rank.py +991 -0
- package/scripts/search_types.py +129 -16
- package/scripts/seek_locator.py +68 -0
- package/scripts/semantic_evidence.py +391 -0
- package/scripts/serp_guard.py +336 -0
- package/scripts/serp_kill_log.py +74 -0
- package/scripts/single_flight.py +120 -0
- package/scripts/social_engines/__init__.py +10 -0
- package/scripts/social_engines/bilibili_engine.py +131 -0
- package/scripts/social_engines/hackernews_engine.py +113 -0
- package/scripts/social_engines/reddit_engine.py +172 -0
- package/scripts/social_engines/twitter_engine.py +397 -0
- package/scripts/social_engines/v2ex_engine.py +101 -0
- package/scripts/social_engines/weibo_engine.py +128 -0
- package/scripts/social_engines/xiaohongshu_engine.py +137 -0
- package/scripts/social_engines/zhihu_engine.py +139 -0
- package/scripts/social_research.py +201 -0
- package/scripts/sogou_weixin_browser.py +318 -0
- package/scripts/source_ledger.py +249 -0
- package/scripts/stage_timing.py +58 -0
- package/scripts/stats_cli.py +107 -0
- package/scripts/straggler_policy.py +60 -0
- package/scripts/stylometry_detector.py +231 -0
- package/scripts/sync_backends.py +353 -0
- package/scripts/sync_installs.py +29 -0
- package/scripts/template_cli.py +127 -0
- package/scripts/text_lexicon.py +72 -0
- package/scripts/tfidf_router.py +109 -37
- package/scripts/time_utils.py +233 -0
- package/scripts/topic_research_profiles.py +643 -0
- package/scripts/train.py +737 -0
- package/scripts/tweet.py +596 -0
- package/scripts/url_canon.py +217 -0
- package/scripts/url_optimize.py +56 -0
- package/scripts/url_safety.py +236 -0
- package/scripts/usage_log.py +116 -0
- package/scripts/usage_log_clean.py +113 -0
- package/scripts/v2ex_nodes.py +311 -0
- package/scripts/variant_recall.py +188 -0
- package/scripts/watch.py +205 -0
- package/scripts/wechat_article.py +258 -0
- package/scripts/wide_research.py +447 -0
- package/scripts/wx.py +349 -0
- package/scripts/x_article_fetch.py +141 -0
- package/scripts/x_search.py +234 -0
- package/scripts/x_trending.py +189 -0
- package/scripts/yaml_load.py +67 -0
- package/scripts/zhihu_quota.py +139 -0
- package/sub-skills/ego-search/SKILL.md +253 -0
- package/sub-skills/ego-search/scripts/ego_search.py +800 -0
- package/sub-skills/ego-search/scripts/merge.py +226 -0
- package/sub-skills/ego-search/scripts/quality.py +64 -0
- package/sub-skills/ego-search/scripts/runtime.py +172 -0
- package/sub-skills/ego-search/scripts/safety.py +109 -0
- package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
- package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
- package/sub-skills/js-run/SKILL.md +67 -0
- package/sub-skills/js-run/scripts/jsrun.py +112 -0
- package/sub-skills/js-run/scripts/shim.js +188 -0
- package/sub-skills/local-search/SKILL.md +50 -53
- package/sub-skills/local-search/bm25.py +186 -0
- package/sub-skills/local-search/cli_runner.py +331 -0
- package/sub-skills/local-search/config.yaml +152 -55
- package/sub-skills/local-search/engine_registry.py +91 -26
- package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
- package/sub-skills/local-search/local_search_adapter.py +13 -2
- package/sub-skills/local-search/parse_maps.yaml +40 -42
- package/sub-skills/local-search/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +716 -242
- package/sub-skills/local-search/smart_router.py +47 -7
- package/sub-skills/local-seek/SKILL.md +110 -0
- package/sub-skills/local-seek/config/domains.yaml +20 -0
- package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
- package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
- package/sub-skills/local-seek/scripts/seek.py +1465 -0
- package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
- package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
- package/sub-skills/local-seek/scripts/seek_rank.py +197 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""engine_registry.py — local-search
|
|
2
|
+
"""engine_registry.py — local-search 引擎注册中心(唯一来源)
|
|
3
3
|
|
|
4
4
|
读取 sub-skills/local-search/config.yaml 与 parse_maps.yaml,维护 24+ 本地引擎的
|
|
5
5
|
元数据、分类与可用状态。新增引擎只需改 YAML,无需改代码。
|
|
@@ -11,6 +11,7 @@ import functools
|
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
|
+
import sys
|
|
14
15
|
import time
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any
|
|
@@ -29,6 +30,11 @@ SKILL_DIR = Path(__file__).resolve().parent
|
|
|
29
30
|
CONFIG_PATH = SKILL_DIR / "config.yaml"
|
|
30
31
|
PARSE_MAPS_PATH = SKILL_DIR / "parse_maps.yaml"
|
|
31
32
|
|
|
33
|
+
# 健康条目陈旧线(与 _load_health/_save_health 共用):30 天未探针的记录对路由
|
|
34
|
+
# 已无参考价值,读入与写回两侧都要过滤——长驻进程的内存快照是全量写回的,
|
|
35
|
+
# 只在读取侧过滤,两天后一次保存就会把 31 天的陈旧条目复活。
|
|
36
|
+
_HEALTH_STALE_S = 30 * 86400
|
|
37
|
+
|
|
32
38
|
# 领域分类(与 config.yaml 中 engines[*].category 对应)
|
|
33
39
|
DEFAULT_CATEGORIES = [
|
|
34
40
|
"web_general",
|
|
@@ -40,30 +46,40 @@ DEFAULT_CATEGORIES = [
|
|
|
40
46
|
"vertical",
|
|
41
47
|
]
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"too many requests", "rate limit",
|
|
48
|
-
]
|
|
49
|
+
def _ensure_root_scripts() -> None:
|
|
50
|
+
_root_scripts = SKILL_DIR.parents[1] / "scripts"
|
|
51
|
+
if _root_scripts.exists() and str(_root_scripts) not in sys.path:
|
|
52
|
+
sys.path.append(str(_root_scripts))
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
def _load_yaml(path: Path) -> dict[str, Any]:
|
|
52
|
-
if yaml is None:
|
|
53
|
-
raise ImportError("缺少 PyYAML,请安装:pip install pyyaml")
|
|
54
56
|
try:
|
|
57
|
+
_ensure_root_scripts()
|
|
58
|
+
from yaml_load import load as _yaml_load
|
|
55
59
|
with path.open("r", encoding="utf-8") as f:
|
|
56
|
-
data =
|
|
60
|
+
data = _yaml_load(f)
|
|
57
61
|
return data if isinstance(data, dict) else {}
|
|
58
|
-
except Exception
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
except Exception:
|
|
63
|
+
if yaml is None:
|
|
64
|
+
raise ImportError("缺少 PyYAML,请安装:pip install pyyaml")
|
|
65
|
+
try:
|
|
66
|
+
with path.open("r", encoding="utf-8") as f:
|
|
67
|
+
data = yaml.safe_load(f)
|
|
68
|
+
return data if isinstance(data, dict) else {}
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"加载 YAML 失败 {path}: {e}")
|
|
71
|
+
return {}
|
|
61
72
|
|
|
62
73
|
|
|
63
74
|
def _default_health_state_path() -> Path:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
try:
|
|
76
|
+
_ensure_root_scripts()
|
|
77
|
+
from argo_paths import state_path
|
|
78
|
+
return state_path("local_search_health.json")
|
|
79
|
+
except Exception:
|
|
80
|
+
base = Path(os.path.expanduser("~/.cache/unified-search"))
|
|
81
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
return base / "local_search_health.json"
|
|
67
83
|
|
|
68
84
|
|
|
69
85
|
class EngineRegistry:
|
|
@@ -77,7 +93,8 @@ class EngineRegistry:
|
|
|
77
93
|
):
|
|
78
94
|
self.config_path = Path(config_path) if config_path else CONFIG_PATH
|
|
79
95
|
self.parse_maps_path = Path(parse_maps_path) if parse_maps_path else PARSE_MAPS_PATH
|
|
80
|
-
self.
|
|
96
|
+
self._explicit_health_path = Path(health_state_path) if health_state_path else None
|
|
97
|
+
self._loaded_health_path: Path | None = None
|
|
81
98
|
self._config_mtime: float = 0.0
|
|
82
99
|
self._parse_mtime: float = 0.0
|
|
83
100
|
self._config: dict[str, Any] = {}
|
|
@@ -85,6 +102,19 @@ class EngineRegistry:
|
|
|
85
102
|
self._health: dict[str, Any] = {}
|
|
86
103
|
self._load()
|
|
87
104
|
|
|
105
|
+
@property
|
|
106
|
+
def health_state_path(self) -> Path:
|
|
107
|
+
return self._explicit_health_path or _default_health_state_path()
|
|
108
|
+
|
|
109
|
+
@health_state_path.setter
|
|
110
|
+
def health_state_path(self, val: Path | str | None) -> None:
|
|
111
|
+
self._explicit_health_path = Path(val) if val else None
|
|
112
|
+
|
|
113
|
+
def _sync_health_path(self) -> None:
|
|
114
|
+
cur = self.health_state_path
|
|
115
|
+
if self._loaded_health_path != cur:
|
|
116
|
+
self._health = self._load_health()
|
|
117
|
+
|
|
88
118
|
def _load(self) -> None:
|
|
89
119
|
"""加载配置、解析映射和健康状态缓存。"""
|
|
90
120
|
cfg = _load_yaml(self.config_path)
|
|
@@ -99,22 +129,44 @@ class EngineRegistry:
|
|
|
99
129
|
self._health = self._load_health()
|
|
100
130
|
|
|
101
131
|
def _load_health(self) -> dict[str, Any]:
|
|
102
|
-
|
|
132
|
+
hp = self.health_state_path
|
|
133
|
+
self._loaded_health_path = hp
|
|
134
|
+
if not hp.exists():
|
|
103
135
|
return {}
|
|
104
136
|
try:
|
|
105
|
-
with
|
|
137
|
+
with hp.open("r", encoding="utf-8") as f:
|
|
106
138
|
data = json.load(f)
|
|
107
139
|
if isinstance(data, dict):
|
|
108
|
-
|
|
140
|
+
# 陈旧条目清理(2026-09-29):健康文件此前只增不减——实测
|
|
141
|
+
# 有条目 49 天未刷新、还有测试引擎名残留。30 天未探针的记录
|
|
142
|
+
# 对路由已无参考价值(引擎配置都可能换过),读入时丢弃。
|
|
143
|
+
cutoff = time.time() - _HEALTH_STALE_S
|
|
144
|
+
return {k: v for k, v in data.items()
|
|
145
|
+
if isinstance(v, dict) and v.get("last_checked", 0) >= cutoff}
|
|
109
146
|
except Exception as e:
|
|
110
147
|
logger.warning(f"加载健康状态失败: {e}")
|
|
111
148
|
return {}
|
|
112
149
|
|
|
113
150
|
def _save_health(self) -> None:
|
|
114
151
|
try:
|
|
115
|
-
self.health_state_path
|
|
116
|
-
|
|
117
|
-
|
|
152
|
+
hp = self.health_state_path
|
|
153
|
+
self._loaded_health_path = hp
|
|
154
|
+
# 写回前套用与 _load_health 相同的陈旧过滤:内存快照是全量写回的,
|
|
155
|
+
# 不过滤会让 30 天未刷新的条目复活并覆盖他进程新写的条目。
|
|
156
|
+
cutoff = time.time() - _HEALTH_STALE_S
|
|
157
|
+
payload = {k: v for k, v in self._health.items()
|
|
158
|
+
if isinstance(v, dict) and v.get("last_checked", 0) >= cutoff}
|
|
159
|
+
hp.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
try:
|
|
161
|
+
_ensure_root_scripts()
|
|
162
|
+
from argo_paths import atomic_write_json, file_lock
|
|
163
|
+
with file_lock(hp):
|
|
164
|
+
atomic_write_json(hp, payload)
|
|
165
|
+
except ImportError:
|
|
166
|
+
tmp = hp.with_name(hp.name + f".{os.getpid()}.tmp")
|
|
167
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
168
|
+
json.dump(payload, f, ensure_ascii=False, indent=2)
|
|
169
|
+
os.replace(tmp, hp)
|
|
118
170
|
except Exception as e:
|
|
119
171
|
logger.warning(f"保存健康状态失败: {e}")
|
|
120
172
|
|
|
@@ -150,6 +202,7 @@ class EngineRegistry:
|
|
|
150
202
|
|
|
151
203
|
def get_engine(self, name: str) -> dict[str, Any] | None:
|
|
152
204
|
"""获取指定引擎的完整配置(含运行时可用状态)。"""
|
|
205
|
+
self._sync_health_path()
|
|
153
206
|
spec = self.engines.get(name)
|
|
154
207
|
if spec is None:
|
|
155
208
|
return None
|
|
@@ -211,26 +264,37 @@ class EngineRegistry:
|
|
|
211
264
|
# ── 可用性管理 ───────────────────────────────────────────────────────────────
|
|
212
265
|
|
|
213
266
|
def get_health(self, name: str) -> dict[str, Any]:
|
|
267
|
+
self._sync_health_path()
|
|
214
268
|
return dict(self._health.get(name, {}))
|
|
215
269
|
|
|
216
|
-
def update_availability(self, name: str, available: bool,
|
|
217
|
-
|
|
270
|
+
def update_availability(self, name: str, available: bool,
|
|
271
|
+
persist: bool = True, **extra: Any) -> None:
|
|
272
|
+
"""更新单个引擎的可用状态。
|
|
273
|
+
|
|
274
|
+
persist=False 时只改内存、不落盘——批量更新方(run_health_check 一轮
|
|
275
|
+
N 个引擎)用它把 N 次全文件重写收成 1 次,由调用方最后统一 _save_health。
|
|
276
|
+
"""
|
|
277
|
+
self._sync_health_path()
|
|
218
278
|
now = time.time()
|
|
219
279
|
record = self._health.setdefault(name, {})
|
|
220
280
|
record["last_checked"] = now
|
|
221
281
|
record["available"] = available
|
|
222
282
|
if available:
|
|
223
283
|
record["consecutive_failures"] = 0
|
|
284
|
+
# 连续成功计数:健康门自适应 TTL 的依据(稳定引擎拉长探针间隔)
|
|
285
|
+
record["consecutive_ok"] = record.get("consecutive_ok", 0) + 1
|
|
224
286
|
record["last_ok"] = now
|
|
225
287
|
record["fail_reason"] = None
|
|
226
288
|
else:
|
|
227
289
|
record["consecutive_failures"] = record.get("consecutive_failures", 0) + 1
|
|
290
|
+
record["consecutive_ok"] = 0
|
|
228
291
|
if extra.get("fail_reason"):
|
|
229
292
|
record["fail_reason"] = extra["fail_reason"]
|
|
230
293
|
for k, v in extra.items():
|
|
231
294
|
if k not in ("last_checked", "available", "consecutive_failures"):
|
|
232
295
|
record[k] = v
|
|
233
|
-
|
|
296
|
+
if persist:
|
|
297
|
+
self._save_health()
|
|
234
298
|
|
|
235
299
|
def bulk_update_availability(self, updates: dict[str, dict[str, Any]]) -> None:
|
|
236
300
|
"""批量更新可用状态。"""
|
|
@@ -238,6 +302,7 @@ class EngineRegistry:
|
|
|
238
302
|
self.update_availability(name, data.get("available", False), **data)
|
|
239
303
|
|
|
240
304
|
def is_available(self, name: str) -> bool:
|
|
305
|
+
self._sync_health_path()
|
|
241
306
|
eng = self.get_engine(name)
|
|
242
307
|
if eng is not None:
|
|
243
308
|
return bool(eng.get("available", True))
|
|
@@ -17,6 +17,8 @@ import json
|
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
20
22
|
import time
|
|
21
23
|
import urllib.error
|
|
22
24
|
import urllib.parse
|
|
@@ -26,6 +28,11 @@ from typing import Any
|
|
|
26
28
|
|
|
27
29
|
from engine_registry import EngineRegistry, get_registry, update_availability
|
|
28
30
|
|
|
31
|
+
# scripts/ 入路径(net_proxy 出口需要;append 不抢占子技能同名模块的优先级)
|
|
32
|
+
_ROOT_SCRIPTS = Path(__file__).resolve().parents[2] / "scripts"
|
|
33
|
+
if _ROOT_SCRIPTS.exists() and str(_ROOT_SCRIPTS) not in sys.path:
|
|
34
|
+
sys.path.append(str(_ROOT_SCRIPTS))
|
|
35
|
+
|
|
29
36
|
logger = logging.getLogger("local_search.health_check")
|
|
30
37
|
if not logger.handlers:
|
|
31
38
|
logger.setLevel(logging.WARNING)
|
|
@@ -41,6 +48,21 @@ ANTI_BOT_MARKERS = [
|
|
|
41
48
|
# HTTP 状态码分组
|
|
42
49
|
RETRYABLE_STATUS = {429, 503, 502, 504}
|
|
43
50
|
|
|
51
|
+
# 自适应 TTL 上限(秒)
|
|
52
|
+
_MAX_HEALTH_TTL_S = 3600.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _effective_ttl(rec: dict[str, Any], base_ttl_s: float) -> float:
|
|
56
|
+
"""连续成功 → 探测间隔指数放宽(×2^min(streak,3),硬上限 60min)。
|
|
57
|
+
|
|
58
|
+
探针不是免费的:HTTP canary 是直连上游的真实请求(最坏 8s 墙钟,且
|
|
59
|
+
每 5min 对 baidu/sogou/360 这类激进反爬源发一次固定「test」查询,纯增
|
|
60
|
+
封号风险)。稳定可用的引擎不需要高频确认;任何一次失败 consecutive_ok
|
|
61
|
+
归零,立即回到基准 TTL。
|
|
62
|
+
"""
|
|
63
|
+
streak = int(rec.get("consecutive_ok", 0) or 0)
|
|
64
|
+
return min(base_ttl_s * (2 ** min(max(streak, 0), 3)), _MAX_HEALTH_TTL_S)
|
|
65
|
+
|
|
44
66
|
|
|
45
67
|
def _now() -> float:
|
|
46
68
|
return time.time()
|
|
@@ -96,9 +118,19 @@ def _fetch_probe(
|
|
|
96
118
|
req_headers.setdefault("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
|
97
119
|
|
|
98
120
|
req = urllib.request.Request(url, headers=req_headers, method=method)
|
|
121
|
+
# 出口统一走 net_proxy.open_url(与 search_v3._fetch 同语义):裸 urlopen
|
|
122
|
+
# 不认 argo 在 config.yaml network.proxy 里配置的 url/rules——「必须经代理
|
|
123
|
+
# 才能出网」的环境里探针全部连不上,所有 HTTP 引擎被判 unavailable,
|
|
124
|
+
# fast/budget 路由随后被静默清空,症状与「源挂了」完全一样(主仓
|
|
125
|
+
# issue #13 同形态,13 个模块已修,这里是漏网的一个)。scripts/ 不在
|
|
126
|
+
# 路径(独立打包形态)时回退裸 urlopen,行为与改动前一致。
|
|
127
|
+
try:
|
|
128
|
+
from net_proxy import open_url
|
|
129
|
+
except ImportError:
|
|
130
|
+
open_url = urllib.request.urlopen # type: ignore[assignment]
|
|
99
131
|
t0 = time.time()
|
|
100
132
|
try:
|
|
101
|
-
with
|
|
133
|
+
with open_url(req, timeout=timeout) as resp:
|
|
102
134
|
raw = resp.read()
|
|
103
135
|
try:
|
|
104
136
|
text = raw.decode("utf-8", errors="replace")
|
|
@@ -149,6 +181,73 @@ def _parse_success(engine_name: str, text: str, fmt: str, registry: EngineRegist
|
|
|
149
181
|
return len(text) > 200
|
|
150
182
|
|
|
151
183
|
|
|
184
|
+
def _check_cli_engine(engine_name: str, spec: dict[str, Any],
|
|
185
|
+
timeout: float = 8,
|
|
186
|
+
canary_query: str = "test", n: int = 1) -> dict[str, Any]:
|
|
187
|
+
"""CLI 引擎健康探针:跑一次真实 canary 查询,校验「能出结果」。
|
|
188
|
+
|
|
189
|
+
2026-10-06 修复假阳性:此前只跑 `ddgs --help`——二进制存在即判
|
|
190
|
+
available。ddgs 9.14.4 移除 bing text 后端、brave/yahoo 对真实查询
|
|
191
|
+
稳定 0 结果后,`--help` 全绿而搜索全灭,死引擎反复混入组合(本地
|
|
192
|
+
兜底链 4.5-8.3s 的结构性成因之一)。探针与搜索同路径(复用
|
|
193
|
+
search_v3._run_cli_engine),canary 拿不到 ≥1 条结果即失败。
|
|
194
|
+
代价:一次真实查询的墙钟(仅健康记录过期时发生,TTL 5min 起)。
|
|
195
|
+
search_v3 不可导入时退化为 --help 探测(fail-open,不崩探针链)。
|
|
196
|
+
"""
|
|
197
|
+
cli_cmd = spec.get("cli_command", "")
|
|
198
|
+
if not cli_cmd:
|
|
199
|
+
return {"name": engine_name, "url": "", "status": None,
|
|
200
|
+
"latency_ms": 0, "parse_ok": False, "text_sample": "",
|
|
201
|
+
"fail_reason": "missing_cli_command", "available": False}
|
|
202
|
+
# which 短路:二进制不在 PATH 上时不再白付一次 --help 子进程 spawn
|
|
203
|
+
# (ddgs 冷启实测 0.35s)。与 search_v3._check_cli_available 同判据;
|
|
204
|
+
# shim 损坏(能 which 到但跑不动)仍由下方真实调用暴露。
|
|
205
|
+
if shutil.which(cli_cmd) is None:
|
|
206
|
+
return {"name": engine_name, "url": "", "status": None,
|
|
207
|
+
"latency_ms": 0, "parse_ok": False, "text_sample": "",
|
|
208
|
+
"fail_reason": "cli_not_found", "available": False}
|
|
209
|
+
import subprocess
|
|
210
|
+
t0 = time.time()
|
|
211
|
+
fail_reason = None
|
|
212
|
+
ok = False
|
|
213
|
+
text_sample = ""
|
|
214
|
+
try:
|
|
215
|
+
try:
|
|
216
|
+
from search_v3 import _run_cli_engine # 同目录,探针与搜索同路径
|
|
217
|
+
except ImportError:
|
|
218
|
+
_run_cli_engine = None # type: ignore[assignment]
|
|
219
|
+
if _run_cli_engine is not None:
|
|
220
|
+
results, err = _run_cli_engine(spec, canary_query, n,
|
|
221
|
+
min(timeout, 8))
|
|
222
|
+
ok = bool(results)
|
|
223
|
+
if not ok:
|
|
224
|
+
fail_reason = (err or "no_results")[:120]
|
|
225
|
+
text_sample = (err or "")[:300]
|
|
226
|
+
else:
|
|
227
|
+
# 退化路径:search_v3 不可用,只验证可执行(fail-open)。
|
|
228
|
+
# 真正的搜索失败由执行层熔断器记录。
|
|
229
|
+
result = subprocess.run(
|
|
230
|
+
[cli_cmd, "--help"],
|
|
231
|
+
capture_output=True, text=True, timeout=min(timeout, 5),
|
|
232
|
+
encoding="utf-8", errors="replace", # Windows GBK locale 下 UTF-8 输出会解码崩
|
|
233
|
+
)
|
|
234
|
+
ok = result.returncode == 0
|
|
235
|
+
if not ok:
|
|
236
|
+
fail_reason = f"cli_exit_{result.returncode}"
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
ok, fail_reason = False, "cli_timeout"
|
|
239
|
+
except FileNotFoundError:
|
|
240
|
+
ok, fail_reason = False, "cli_not_found"
|
|
241
|
+
except Exception as e: # 探针自身失败不判引擎死(fail-open):记为软失败
|
|
242
|
+
ok, fail_reason = False, f"probe_error:{e}"[:120]
|
|
243
|
+
return {
|
|
244
|
+
"name": engine_name, "url": "", "status": 200 if ok else None,
|
|
245
|
+
"latency_ms": round((time.time() - t0) * 1000, 2),
|
|
246
|
+
"parse_ok": ok, "text_sample": text_sample,
|
|
247
|
+
"fail_reason": fail_reason, "available": ok,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
152
251
|
def check_engine(
|
|
153
252
|
engine_name: str,
|
|
154
253
|
registry: EngineRegistry | None = None,
|
|
@@ -164,6 +263,11 @@ def check_engine(
|
|
|
164
263
|
if not spec.get("enabled", True):
|
|
165
264
|
return {"name": engine_name, "available": False, "fail_reason": "disabled"}
|
|
166
265
|
|
|
266
|
+
# cli 引擎无 HTTP 端点,走 CLI 探针而非 URL 探针
|
|
267
|
+
if spec.get("type") == "cli":
|
|
268
|
+
return _check_cli_engine(engine_name, spec, timeout=timeout,
|
|
269
|
+
canary_query=canary_query, n=n)
|
|
270
|
+
|
|
167
271
|
url, headers = _build_canary_url(spec, canary_query, n)
|
|
168
272
|
method = spec.get("method", "GET")
|
|
169
273
|
status, latency_ms, text, fail_reason = _fetch_probe(url, headers, method=method, timeout=timeout)
|
|
@@ -254,13 +358,18 @@ def run_health_check(
|
|
|
254
358
|
enabled = engine_names if engine_names is not None else reg.list_engines(enabled_only=True)
|
|
255
359
|
|
|
256
360
|
now = _now()
|
|
257
|
-
# TTL
|
|
361
|
+
# TTL 检查:在有效期内直接复用缓存。
|
|
362
|
+
# 按引擎名逐一比对(此前 cached.values() 里的 dict 与 enabled 的 str 比较
|
|
363
|
+
# 恒 False → all(空) 恒 True → 健康状态永不刷新),任一引擎记录缺失或
|
|
364
|
+
# 过期即重新探针。TTL 按连续成功自适应放宽(_effective_ttl)。
|
|
258
365
|
cached = reg._health
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
366
|
+
|
|
367
|
+
def _recent(name: str) -> bool:
|
|
368
|
+
rec = cached.get(name)
|
|
369
|
+
return bool(rec) and (now - rec.get("last_checked", 0)) < _effective_ttl(rec, ttl_minutes * 60)
|
|
370
|
+
|
|
371
|
+
all_recent = bool(enabled) and all(_recent(n) for n in enabled)
|
|
372
|
+
if all_recent:
|
|
264
373
|
return {name: dict(h) for name, h in cached.items() if name in enabled}
|
|
265
374
|
|
|
266
375
|
reports: dict[str, dict[str, Any]] = {}
|
|
@@ -278,8 +387,9 @@ def run_health_check(
|
|
|
278
387
|
for name, report in reports.items():
|
|
279
388
|
previous = reg.get_health(name)
|
|
280
389
|
final_available = apply_threshold(report, previous)
|
|
390
|
+
# available 以位置参数传入,勿再放进 kwargs(此前重复键触发
|
|
391
|
+
# TypeError,被 #⑨ 缓存恒命中掩盖,健康检查从未真正执行)
|
|
281
392
|
update_data = {
|
|
282
|
-
"available": final_available,
|
|
283
393
|
"latency_ms": report.get("latency_ms"),
|
|
284
394
|
"status": report.get("status"),
|
|
285
395
|
"parse_ok": report.get("parse_ok"),
|
|
@@ -289,7 +399,11 @@ def run_health_check(
|
|
|
289
399
|
update_data["success_rate"] = 1.0
|
|
290
400
|
else:
|
|
291
401
|
update_data["success_rate"] = 0.0
|
|
292
|
-
|
|
402
|
+
# persist=False:一轮 N 个引擎收成 1 次落盘(此前每引擎全文件重写一次)
|
|
403
|
+
reg.update_availability(name, final_available, persist=False, **update_data)
|
|
404
|
+
|
|
405
|
+
if reports:
|
|
406
|
+
reg._save_health()
|
|
293
407
|
|
|
294
408
|
return reports
|
|
295
409
|
|
|
@@ -310,8 +424,24 @@ def get_available_engines(
|
|
|
310
424
|
now = _now()
|
|
311
425
|
ttl = reg.settings.get("health_check", {}).get("ttl_minutes", 5) * 60
|
|
312
426
|
cached = reg._health
|
|
313
|
-
|
|
427
|
+
|
|
428
|
+
def _recent(name: str) -> bool:
|
|
429
|
+
rec = cached.get(name)
|
|
430
|
+
# TTL 按连续成功自适应放宽:稳定引擎最多放宽到 60min,
|
|
431
|
+
# 探针墙钟与对反爬源的 canary 流量随之下降一个量级。
|
|
432
|
+
return bool(rec) and (now - rec.get("last_checked", 0)) < _effective_ttl(rec, ttl)
|
|
433
|
+
|
|
434
|
+
# 逐引擎名比对新鲜度(此前 dict in list[str] 恒 False → 恒命中,
|
|
435
|
+
# 健康状态永不刷新)。
|
|
436
|
+
# 2026-10-06:只探**过期**的引擎——此前 all() 判全新鲜,任一过期即
|
|
437
|
+
# 全量重探,稳定引擎(TTL 放宽到 60min)被反复拖进探针浪;探针浪
|
|
438
|
+
# 实测 ~2-4.5s(yahoo 慢空),全量重探把它加到每次调用路径上。
|
|
439
|
+
# 注意 engine_names 在上方已保证非空(None → 全量启用列表)。
|
|
440
|
+
stale = [n for n in engine_names if not _recent(n)]
|
|
441
|
+
if not stale:
|
|
314
442
|
return [n for n in engine_names if reg.is_available(n)]
|
|
443
|
+
run_health_check(registry=reg, engine_names=stale)
|
|
444
|
+
return [n for n in engine_names if reg.is_available(n)]
|
|
315
445
|
run_health_check(registry=reg, engine_names=engine_names)
|
|
316
446
|
return [n for n in engine_names if reg.is_available(n)]
|
|
317
447
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""local_search_adapter.py — local-search 兼容入口
|
|
3
3
|
|
|
4
4
|
保留原 adapter 接口,内部委托 search_v3.search_engines 执行。
|
|
5
|
-
unified-search 可通过 --
|
|
5
|
+
unified-search 可通过 --local-first 调用本入口。
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -29,10 +29,18 @@ def main():
|
|
|
29
29
|
parser.add_argument("--engine", "-e", default="", help="引擎名,多个用逗号分隔")
|
|
30
30
|
parser.add_argument("--n", type=int, default=5, help="每引擎结果数")
|
|
31
31
|
parser.add_argument("--timeout", "-t", type=float, default=None, help="超时秒数")
|
|
32
|
-
|
|
32
|
+
# None = 以 config 的 max_parallel_engines 为真源(与主链进程内路径同口径)
|
|
33
|
+
parser.add_argument("--max-parallel", type=int, default=None)
|
|
33
34
|
parser.add_argument("--no-cache", action="store_true", help="跳过缓存")
|
|
34
35
|
parser.add_argument("--mode", default="fast", choices=["fast", "auto", "deep", "budget"],
|
|
35
36
|
help="unified-search 模式透传")
|
|
37
|
+
parser.add_argument("--since", default=None,
|
|
38
|
+
help="发布时间下限(7d / 2026-08-01),下推到支持时间参数的引擎")
|
|
39
|
+
parser.add_argument("--until", default=None,
|
|
40
|
+
help="发布时间上限(7d / 2026-08-01),下推到支持时间参数的引擎")
|
|
41
|
+
parser.add_argument("--sort", default="relevance",
|
|
42
|
+
choices=["relevance", "oldest", "newest"],
|
|
43
|
+
help="时间排序:relevance=相关度(默认), oldest=最早在前(溯源), newest=最新在前")
|
|
36
44
|
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
37
45
|
args = parser.parse_args()
|
|
38
46
|
|
|
@@ -48,6 +56,9 @@ def main():
|
|
|
48
56
|
max_parallel=args.max_parallel,
|
|
49
57
|
skip_cache=args.no_cache,
|
|
50
58
|
mode=args.mode,
|
|
59
|
+
since=args.since,
|
|
60
|
+
until=args.until,
|
|
61
|
+
sort=args.sort,
|
|
51
62
|
)
|
|
52
63
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
53
64
|
|
|
@@ -15,51 +15,11 @@ html:
|
|
|
15
15
|
url_attr: href
|
|
16
16
|
score: 0.75
|
|
17
17
|
|
|
18
|
-
local_google:
|
|
19
|
-
container: "div.g"
|
|
20
|
-
title: "h3"
|
|
21
|
-
url: "a"
|
|
22
|
-
snippet: "div.VwiC3b, div.s, span.aCOpRe"
|
|
23
|
-
url_attr: href
|
|
24
|
-
score: 0.75
|
|
25
|
-
|
|
26
|
-
local_mojeek:
|
|
27
|
-
container: "li.results-standard-list"
|
|
28
|
-
title: "a.ob"
|
|
29
|
-
url: "a.ob"
|
|
30
|
-
snippet: "p.s"
|
|
31
|
-
url_attr: href
|
|
32
|
-
score: 0.7
|
|
33
|
-
|
|
34
|
-
local_yandex:
|
|
35
|
-
container: "li.serp-item"
|
|
36
|
-
title: "a.Link"
|
|
37
|
-
url: "a.Link"
|
|
38
|
-
snippet: "div.TextContainer"
|
|
39
|
-
url_attr: href
|
|
40
|
-
score: 0.65
|
|
41
|
-
|
|
42
|
-
local_startpage:
|
|
43
|
-
container: "div.result"
|
|
44
|
-
title: "a.result-title"
|
|
45
|
-
url: "a.result-title"
|
|
46
|
-
snippet: "p.result-snippet"
|
|
47
|
-
url_attr: href
|
|
48
|
-
score: 0.7
|
|
49
|
-
|
|
50
|
-
local_duckduckgo:
|
|
51
|
-
container: ".result"
|
|
52
|
-
title: "a.result__a"
|
|
53
|
-
url: "a.result__a"
|
|
54
|
-
snippet: "a.result__snippet"
|
|
55
|
-
url_attr: href
|
|
56
|
-
score: 0.7
|
|
57
|
-
|
|
58
18
|
local_baidu:
|
|
59
|
-
container: ".result, .c-container"
|
|
19
|
+
container: ".result, .c-container, .cosc-card-content-border"
|
|
60
20
|
title: "h3"
|
|
61
21
|
url: "a"
|
|
62
|
-
snippet: ".content-
|
|
22
|
+
snippet: ".c-abstract, .cosc-card-content-border, .cosc-card-content, .summary-text_15QGa, .text_2NOr6, .main-info_4Q_kj, .bk_polysemy_1Ef6j"
|
|
63
23
|
url_attr: href
|
|
64
24
|
score: 0.7
|
|
65
25
|
|
|
@@ -71,6 +31,22 @@ html:
|
|
|
71
31
|
url_attr: href
|
|
72
32
|
score: 0.65
|
|
73
33
|
|
|
34
|
+
local_360:
|
|
35
|
+
container: "li.res-list"
|
|
36
|
+
title: "h3.res-title a"
|
|
37
|
+
url: "h3.res-title a"
|
|
38
|
+
snippet: "p.res-desc"
|
|
39
|
+
url_attr: data-mdurl
|
|
40
|
+
score: 0.7
|
|
41
|
+
|
|
42
|
+
local_jisilu:
|
|
43
|
+
container: ".aw-question-list .aw-item"
|
|
44
|
+
title: "h4 a"
|
|
45
|
+
url: "h4 a"
|
|
46
|
+
snippet: ".aw-text-color-999"
|
|
47
|
+
url_attr: href
|
|
48
|
+
score: 0.6
|
|
49
|
+
|
|
74
50
|
local_stackoverflow:
|
|
75
51
|
container: ".question-summary"
|
|
76
52
|
title: "a.question-hyperlink"
|
|
@@ -95,12 +71,28 @@ html:
|
|
|
95
71
|
url_attr: href
|
|
96
72
|
score: 0.7
|
|
97
73
|
|
|
74
|
+
know_your_meme:
|
|
75
|
+
container: "a.item"
|
|
76
|
+
title: "h3"
|
|
77
|
+
url: "a"
|
|
78
|
+
url_attr: href
|
|
79
|
+
score: 0.7
|
|
80
|
+
|
|
81
|
+
moegirl:
|
|
82
|
+
container: "li.mw-search-result"
|
|
83
|
+
title: "div.mw-search-result-heading a"
|
|
84
|
+
url: "div.mw-search-result-heading a"
|
|
85
|
+
snippet: "div.searchresult"
|
|
86
|
+
url_attr: href
|
|
87
|
+
score: 0.7
|
|
88
|
+
|
|
98
89
|
xml:
|
|
99
90
|
local_arxiv:
|
|
100
91
|
entry_path: ".//{http://www.w3.org/2005/Atom}entry"
|
|
101
92
|
title: "atom:title"
|
|
102
93
|
url: "atom:id"
|
|
103
94
|
snippet: "atom:summary"
|
|
95
|
+
published_at: "atom:published"
|
|
104
96
|
namespaces:
|
|
105
97
|
atom: "http://www.w3.org/2005/Atom"
|
|
106
98
|
|
|
@@ -138,36 +130,42 @@ json:
|
|
|
138
130
|
title: "full_name"
|
|
139
131
|
url: "html_url"
|
|
140
132
|
snippet: "description"
|
|
133
|
+
published_at: "created_at"
|
|
141
134
|
|
|
142
135
|
local_npm:
|
|
143
136
|
items: "objects"
|
|
144
137
|
title: "package.name"
|
|
145
138
|
url: "package.links.npm"
|
|
146
139
|
snippet: "package.description"
|
|
140
|
+
published_at: "package.date"
|
|
147
141
|
|
|
148
142
|
local_gitlab:
|
|
149
143
|
items: "."
|
|
150
144
|
title: "name_with_namespace"
|
|
151
145
|
url: "web_url"
|
|
152
146
|
snippet: "description"
|
|
147
|
+
published_at: "created_at"
|
|
153
148
|
|
|
154
149
|
local_stackoverflow:
|
|
155
150
|
items: "items"
|
|
156
151
|
title: "title"
|
|
157
152
|
url: "link"
|
|
158
153
|
snippet: null
|
|
154
|
+
published_at: "creation_date"
|
|
159
155
|
|
|
160
156
|
local_crossref:
|
|
161
157
|
items: "message.items"
|
|
162
158
|
title: "title"
|
|
163
159
|
url: "URL"
|
|
164
160
|
snippet: "abstract"
|
|
161
|
+
published_at: "message.published-print.date-parts.0"
|
|
165
162
|
|
|
166
163
|
local_semantic_scholar:
|
|
167
164
|
items: "data"
|
|
168
165
|
title: "title"
|
|
169
166
|
url: "url"
|
|
170
167
|
snippet: "abstract"
|
|
168
|
+
published_at: "publicationDate"
|
|
171
169
|
|
|
172
170
|
local_pubmed:
|
|
173
171
|
items: "esearchresult.idlist"
|