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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""漂移阈值判定:把夜间「重录 vs 基线」的比较结果变成退出码。
|
|
3
|
+
|
|
4
|
+
## 为什么单独成文件
|
|
5
|
+
|
|
6
|
+
判定逻辑原来写在 nightly.yml 的 heredoc 里:语法对错只能在推上去之后知道,
|
|
7
|
+
阈值改松改紧也没人测。抽出来后有两个好处——本地能跑同一套判定,用例能锁住
|
|
8
|
+
「跌多少算跌」这条契约(tests/test_ci_workflows.py)。
|
|
9
|
+
|
|
10
|
+
## 判什么、为什么只判这两样
|
|
11
|
+
|
|
12
|
+
- **结果条数**:少 1 条允许(工具源单条抖动很常见),少 2 条以上算漂移;
|
|
13
|
+
- **输出体积**:超过基线 20% 算漂移(体积是 Agent 真金白银付的上下文)。
|
|
14
|
+
|
|
15
|
+
`not_recorded`(重放时要了一个当时没调过的引擎)**不计入**:录制只保留真正
|
|
16
|
+
被调到的引擎,而重放是瞬时完成,早停时机与真跑不同,这是录制机制的正常边界,
|
|
17
|
+
当成回归报会制造长期噪音。它仍然会打印出来,供人看。
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
python3 scripts/ci_drift_gate.py --baseline 基线报告.json --current 本次报告.json
|
|
21
|
+
退出码:0 通过;1 有漂移;2 输入缺失/格式不对(这两件事必须分开,混在一起
|
|
22
|
+
会让人以为「红了=质量掉了」)。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
KEPT_SLACK = 1 # 允许少 1 条
|
|
34
|
+
BYTES_GROWTH = 1.2 # 体积上限 = 基线 × 1.2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load(path: Path) -> dict[str, Any]:
|
|
38
|
+
doc = json.loads(path.read_text(encoding="utf-8"))
|
|
39
|
+
# save_baseline 存的是 {"_meta":…, "report":{…}};evaluate_all 直接给报告本体
|
|
40
|
+
return doc.get("report", doc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def violations(base: dict[str, Any], cur: dict[str, Any]) -> list[str]:
|
|
44
|
+
by_id = {c.get("id"): c for c in (base.get("cases") or [])}
|
|
45
|
+
cur_ids = {c.get("id") for c in (cur.get("cases") or [])}
|
|
46
|
+
out: list[str] = []
|
|
47
|
+
for case in cur.get("cases") or []:
|
|
48
|
+
old = by_id.get(case.get("id"))
|
|
49
|
+
if not old:
|
|
50
|
+
continue # 新增的用例没有基线,不算漂移
|
|
51
|
+
cid = case.get("id")
|
|
52
|
+
kept, old_kept = case.get("kept", 0), old.get("kept", 0)
|
|
53
|
+
if kept < old_kept - KEPT_SLACK:
|
|
54
|
+
out.append(f"{cid}: 结果 {kept} 条,基线 {old_kept} 条")
|
|
55
|
+
old_bytes = old.get("bytes", 0)
|
|
56
|
+
if old_bytes and case.get("bytes", 0) > old_bytes * BYTES_GROWTH:
|
|
57
|
+
out.append(
|
|
58
|
+
f"{cid}: 输出 {case.get('bytes')}B,超过基线 {old_bytes}B×{BYTES_GROWTH}")
|
|
59
|
+
# 基线里有、本次没有:不是「通过」,是这条根本没被录到(查询集被改、
|
|
60
|
+
# 或录制整条失败)。2026-10-22 本地实跑踩过:只剩 1 条时判定照样退 0。
|
|
61
|
+
for cid in by_id:
|
|
62
|
+
if cid and cid not in cur_ids:
|
|
63
|
+
out.append(f"{cid}: 本次报告里没有这条(查询集被改了,还是录制失败?)")
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main() -> int:
|
|
68
|
+
ap = argparse.ArgumentParser(description="夜间漂移阈值判定")
|
|
69
|
+
ap.add_argument("--baseline", required=True)
|
|
70
|
+
ap.add_argument("--current", required=True)
|
|
71
|
+
args = ap.parse_args()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
base = _load(Path(args.baseline))
|
|
75
|
+
cur = _load(Path(args.current))
|
|
76
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
77
|
+
print(f"读不到报告:{type(e).__name__}: {e}", file=sys.stderr)
|
|
78
|
+
return 2
|
|
79
|
+
|
|
80
|
+
bad = violations(base, cur)
|
|
81
|
+
if bad:
|
|
82
|
+
print("\n".join(bad), file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
print(f"通过:{len(cur.get('cases') or [])} 条,漏录 "
|
|
85
|
+
f"{cur.get('total_missing', 0)} 个引擎(信息项,不算漂移)")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
sys.exit(main())
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""circuit_breaker.py — 引擎熔断 + 查询级负缓存
|
|
3
|
+
|
|
4
|
+
吸收 Hound 的 circuit-breaker 思路:
|
|
5
|
+
- 连续失败 / 空结果 → 打开熔断,冷却期内跳过该引擎
|
|
6
|
+
- 查询级负缓存:同一 query+engine 短 TTL 内不再打网络
|
|
7
|
+
|
|
8
|
+
状态持久化:<状态目录>/circuit_breaker.json(由 argo_paths 派生)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
# 本地状态目录唯一来源(env ARGO_STATE_DIR → config cache.db_path 父目录 → 旧路径)
|
|
22
|
+
import argo_paths as _paths
|
|
23
|
+
|
|
24
|
+
STATE_PATH = str(_paths.state_path("circuit_breaker.json"))
|
|
25
|
+
|
|
26
|
+
# 熔断参数
|
|
27
|
+
FAILURE_THRESHOLD = 2 # 连续失败次数
|
|
28
|
+
OPEN_SECONDS = 60 # 熔断冷却
|
|
29
|
+
EMPTY_NEGATIVE_TTL = 45 # 空结果负缓存(秒)
|
|
30
|
+
ERROR_NEGATIVE_TTL = 30 # 错误负缓存(秒)
|
|
31
|
+
HALF_OPEN_PROBE = True # 冷却后允许一次探测
|
|
32
|
+
|
|
33
|
+
# 自适应禁用(v2.7)
|
|
34
|
+
DISABLE_AFTER_OPENS = 3 # 连续 open 达此次数 → 自动禁用(持久跳过)
|
|
35
|
+
DISABLE_COOLDOWN_SECONDS = 3600 # 禁用后 1h 内不自动恢复(避免频繁探测)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CircuitBreaker:
|
|
39
|
+
"""进程内 + 磁盘共享的引擎熔断器。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, state_path: str = STATE_PATH):
|
|
42
|
+
self._path = state_path
|
|
43
|
+
self._lock = threading.RLock()
|
|
44
|
+
self._engines: dict[str, dict[str, Any]] = {}
|
|
45
|
+
self._neg: dict[str, dict[str, Any]] = {} # key → {expires, status}
|
|
46
|
+
self._last_save_ts: float = 0.0 # 最近一次成功落盘时刻(观测用)
|
|
47
|
+
self._sig: tuple[int, int] | None = None
|
|
48
|
+
self._load()
|
|
49
|
+
|
|
50
|
+
def _file_sig(self) -> tuple[int, int] | None:
|
|
51
|
+
try:
|
|
52
|
+
st = os.stat(self._path)
|
|
53
|
+
return (st.st_mtime_ns, st.st_size)
|
|
54
|
+
except OSError:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def _fresh_locked(self) -> None:
|
|
58
|
+
sig = self._file_sig()
|
|
59
|
+
if sig != self._sig and (sig is not None or self._sig is not None):
|
|
60
|
+
if sig is None:
|
|
61
|
+
self._engines = {}
|
|
62
|
+
self._sig = None
|
|
63
|
+
else:
|
|
64
|
+
self._load()
|
|
65
|
+
|
|
66
|
+
def _load(self) -> None:
|
|
67
|
+
try:
|
|
68
|
+
# 直接 open 而非 os.path.exists + open:后者有 check-then-act 竞态
|
|
69
|
+
# (文件可能在两者之间被删除),且 FileNotFoundError 已被外层捕获。
|
|
70
|
+
# with 关闭句柄:原 open(...).read() 靠 CPython 引用计数兜住,
|
|
71
|
+
# 在别的解释器实现上会泄漏 fd(SIM115)
|
|
72
|
+
with open(self._path, encoding="utf-8") as f:
|
|
73
|
+
data = json.loads(f.read())
|
|
74
|
+
self._engines = data.get("engines") or {}
|
|
75
|
+
self._sig = self._file_sig()
|
|
76
|
+
# 负缓存仅进程内有效,不从磁盘恢复(避免长期脏状态)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
self._sig = None # 文件不存在,保留空态
|
|
79
|
+
except Exception as e:
|
|
80
|
+
# 解析失败**不清空**已有记忆:清空 = 已被判死的源重新进路由、
|
|
81
|
+
# auto-disable 全部重置,而且没有任何提示(用户看到的是「今天
|
|
82
|
+
# 网络又不行了」)。与 quota.py 的处理方式保持一致——那边
|
|
83
|
+
# 明确「损坏不清空,保留旧状态」,这里此前相反:坏文件一出现就悄悄清零。
|
|
84
|
+
# 首次加载(_engines 为空)时保留空态是安全的:没有记忆可丢。
|
|
85
|
+
import sys as _sys
|
|
86
|
+
print(f"[circuit-breaker] 状态文件不可解析({type(e).__name__}),"
|
|
87
|
+
f"保留内存态 {len(self._engines)} 条:{self._path}",
|
|
88
|
+
file=_sys.stderr)
|
|
89
|
+
|
|
90
|
+
# 引擎熔断 ────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
def status(self, engine: str) -> dict[str, Any]:
|
|
93
|
+
"""只读查询引擎熔断状态(不推进 half-open 探测、不写入文件)。
|
|
94
|
+
|
|
95
|
+
供路由层做「配额/熔断感知沉底」:主引擎熔断打开时自动切换到
|
|
96
|
+
相近备选,正常路径组合集合不变,缓存键不变,速度零影响。
|
|
97
|
+
"""
|
|
98
|
+
with self._lock:
|
|
99
|
+
self._fresh_locked()
|
|
100
|
+
st = self._engines.get(engine) or {}
|
|
101
|
+
state = st.get("state", "closed")
|
|
102
|
+
opened_at = float(st.get("opened_at") or 0)
|
|
103
|
+
return {
|
|
104
|
+
"state": state,
|
|
105
|
+
"failures": int(st.get("failures") or 0),
|
|
106
|
+
"opened_at": opened_at,
|
|
107
|
+
"last_kind": st.get("last_kind"),
|
|
108
|
+
"last_attribution": st.get("last_attribution"),
|
|
109
|
+
# 用 time.time()(墙钟)而非 time.monotonic():熔断状态持久化到文件,
|
|
110
|
+
# 跨进程恢复时 opened_at 必须可比较。monotonic 在不同进程间基准不同,
|
|
111
|
+
# 换用会导致跨进程冷却判断错误。时钟回拨风险可控(只会让冷却提前结束)。
|
|
112
|
+
"cooldown_remain": max(0, int(OPEN_SECONDS - (time.time() - opened_at)))
|
|
113
|
+
if state == "open" else 0,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def allow(self, engine: str) -> tuple[bool, str]:
|
|
117
|
+
"""是否允许调用该引擎。返回 (allowed, reason)。"""
|
|
118
|
+
with self._lock:
|
|
119
|
+
self._fresh_locked()
|
|
120
|
+
st = self._engines.get(engine) or {}
|
|
121
|
+
state = st.get("state", "closed")
|
|
122
|
+
opened_at = float(st.get("opened_at") or 0)
|
|
123
|
+
|
|
124
|
+
# 自适应禁用:disabled 引擎直接拒绝(不再 half-open 探测,省超时)。
|
|
125
|
+
# 但冷却期(DISABLE_COOLDOWN_SECONDS)过后自动转 half_open 探测,
|
|
126
|
+
# 避免引擎恢复后永久无入口(B4)。disabled_at=0 视为旧数据,直接放行探测。
|
|
127
|
+
if state == "disabled":
|
|
128
|
+
disabled_at = float(st.get("disabled_at") or 0)
|
|
129
|
+
if disabled_at == 0 or \
|
|
130
|
+
(time.time() - disabled_at) >= DISABLE_COOLDOWN_SECONDS:
|
|
131
|
+
def _reenable_probe() -> None:
|
|
132
|
+
# 锁内重读后再改:_reload_engines 刚整体替换过
|
|
133
|
+
# self._engines,闭包里的旧 st 会把另一进程刚落盘的
|
|
134
|
+
# 状态盖回去(017b01a 修的那类丢更新在 allow() 的漏网处)。
|
|
135
|
+
fresh = self._engines.get(engine) or {}
|
|
136
|
+
fresh["state"] = "half_open"
|
|
137
|
+
fresh["disabled_at"] = time.time() # 本次探测起点,失败则重新计冷却
|
|
138
|
+
self._engines[engine] = fresh
|
|
139
|
+
self._mutate_locked(_reenable_probe)
|
|
140
|
+
return True, "half_open_reenable"
|
|
141
|
+
return False, "auto_disabled"
|
|
142
|
+
|
|
143
|
+
if state == "open":
|
|
144
|
+
# 冷却期已过 → half-open 探测(除非已连续多次 open 触发自动禁用)
|
|
145
|
+
if time.time() - opened_at >= OPEN_SECONDS:
|
|
146
|
+
opens = int(st.get("opens") or 0)
|
|
147
|
+
disabled_at = float(st.get("disabled_at") or 0)
|
|
148
|
+
# 禁用条件:连续 open 达阈值,且距离上次禁用已超冷却(首次 disabled_at=0 视为可禁用)
|
|
149
|
+
can_disable = opens >= DISABLE_AFTER_OPENS and \
|
|
150
|
+
(disabled_at == 0 or
|
|
151
|
+
(time.time() - disabled_at) >= DISABLE_COOLDOWN_SECONDS)
|
|
152
|
+
if can_disable:
|
|
153
|
+
def _auto_disable() -> None:
|
|
154
|
+
fresh = self._engines.get(engine) or {} # 同上:锁内重读
|
|
155
|
+
fresh["state"] = "disabled"
|
|
156
|
+
fresh["disabled_at"] = time.time()
|
|
157
|
+
self._engines[engine] = fresh
|
|
158
|
+
self._mutate_locked(_auto_disable)
|
|
159
|
+
return False, "auto_disabled"
|
|
160
|
+
# half-open:允许一次探测
|
|
161
|
+
def _half_open() -> None:
|
|
162
|
+
fresh = self._engines.get(engine) or {} # 同上:锁内重读
|
|
163
|
+
fresh["state"] = "half_open"
|
|
164
|
+
self._engines[engine] = fresh
|
|
165
|
+
self._mutate_locked(_half_open)
|
|
166
|
+
return True, "half_open_probe"
|
|
167
|
+
remain = int(OPEN_SECONDS - (time.time() - opened_at))
|
|
168
|
+
return False, f"circuit_open:{remain}s"
|
|
169
|
+
return True, "closed"
|
|
170
|
+
|
|
171
|
+
def _mutate_locked(self, mutator) -> None:
|
|
172
|
+
"""跨进程安全的「重读 → 改 → 写入」序列(与 quota._mutate_locked 同形)。
|
|
173
|
+
|
|
174
|
+
进程内 RLock 只挡得住同进程线程;CLI / MCP server / 评测脚本三者并行时,
|
|
175
|
+
各自在构造期读了一次旧状态、各自 +1、后写者覆盖前写者,计数直接丢失。
|
|
176
|
+
这里在文件锁内重读最新磁盘态再改再写,增量才不丢。fail-open:锁层
|
|
177
|
+
出问题绝不阻断搜索主路径(同 argo_paths.file_lock 的契约)。
|
|
178
|
+
"""
|
|
179
|
+
with self._lock:
|
|
180
|
+
applied = False
|
|
181
|
+
try:
|
|
182
|
+
with _paths.file_lock(Path(self._path)):
|
|
183
|
+
self._reload_engines()
|
|
184
|
+
mutator()
|
|
185
|
+
applied = True
|
|
186
|
+
_paths.atomic_write_json(
|
|
187
|
+
Path(self._path),
|
|
188
|
+
{"engines": self._engines, "updated": time.time()},
|
|
189
|
+
indent=None,
|
|
190
|
+
)
|
|
191
|
+
self._sig = self._file_sig()
|
|
192
|
+
self._last_save_ts = time.time()
|
|
193
|
+
except Exception:
|
|
194
|
+
# 锁/写盘失败:若变更尚未落到内存则补一次,保证本进程内行为
|
|
195
|
+
# 正确;已应用过就绝不再跑(否则计数会翻倍)。丢的只是跨进程
|
|
196
|
+
# 可见性,下一次成功写入会带上。
|
|
197
|
+
if not applied:
|
|
198
|
+
try:
|
|
199
|
+
mutator()
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
def _reload_engines(self) -> None:
|
|
204
|
+
"""重读磁盘上的引擎态(只在文件锁内调用)。解析失败保留内存态。"""
|
|
205
|
+
try:
|
|
206
|
+
# 直接 open 而非 os.path.exists + open:消灭 check-then-act 竞态
|
|
207
|
+
with open(self._path, encoding="utf-8") as f:
|
|
208
|
+
self._engines = (json.loads(f.read()).get("engines") or {})
|
|
209
|
+
self._sig = self._file_sig()
|
|
210
|
+
except FileNotFoundError:
|
|
211
|
+
self._sig = None # 文件不存在,保留内存态
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def record_success(self, engine: str) -> None:
|
|
216
|
+
"""引擎成功返回:本已健康 closed 且无失败计数时内存短路(零文件锁、零写盘)。"""
|
|
217
|
+
with self._lock:
|
|
218
|
+
self._fresh_locked()
|
|
219
|
+
st = self._engines.get(engine)
|
|
220
|
+
if not st or (
|
|
221
|
+
st.get("state", "closed") == "closed"
|
|
222
|
+
and not st.get("failures")
|
|
223
|
+
and not st.get("opens")
|
|
224
|
+
and not st.get("empty_streak")
|
|
225
|
+
):
|
|
226
|
+
if st is None:
|
|
227
|
+
self._engines[engine] = {"state": "closed", "failures": 0, "opens": 0}
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
def _m() -> None:
|
|
231
|
+
self._engines[engine] = {
|
|
232
|
+
"state": "closed",
|
|
233
|
+
"failures": 0,
|
|
234
|
+
"opens": 0, # 重置连续 open 计数
|
|
235
|
+
}
|
|
236
|
+
self._mutate_locked(_m)
|
|
237
|
+
|
|
238
|
+
def record_failure(self, engine: str, kind: str = "error",
|
|
239
|
+
attribution: dict[str, Any] | None = None) -> None:
|
|
240
|
+
"""kind: error | timeout | empty | blocked | rate-limited
|
|
241
|
+
|
|
242
|
+
attribution:可选的失败归因(失败现场记录,含 category/reason/detail)。
|
|
243
|
+
kind 是熔断策略用的粗粒度标签,attribution 是给「为什么坏」用的细粒度
|
|
244
|
+
事实——两者维度不同,不能互相推导(把 kind 当响应文本再归类,只能得到
|
|
245
|
+
unknown)。持久化后 `--list-engines --detail` 才能显示真实原因。
|
|
246
|
+
|
|
247
|
+
empty 语义是「该查询无结果」——查询级信号,不是引擎级故障。
|
|
248
|
+
它仍可触发 60s 短冷却(防止重复打无效源),但不累计 opens,
|
|
249
|
+
因此永远不会驱动 auto-disable(否则聚合引擎/local 源等易空
|
|
250
|
+
引擎会被误判为持续故障而静默禁用)。
|
|
251
|
+
|
|
252
|
+
blocked / rate-limited 语义是「源站行为,不是引擎坏了」——前者是
|
|
253
|
+
请求在到达内容前被拦截(反爬/指纹/挑战页),后者是源端限流。
|
|
254
|
+
同样只做 60s 短冷却、不累计 opens:把封锁/限流当引擎故障累计到
|
|
255
|
+
auto-disable 是封错人——引擎实现没有问题,换客户端形态、
|
|
256
|
+
等冷却或等源站策略变化即可恢复。
|
|
257
|
+
"""
|
|
258
|
+
self._mutate_locked(lambda: self._mutate_failure(engine, kind, attribution))
|
|
259
|
+
|
|
260
|
+
def _mutate_failure(self, engine: str, kind: str,
|
|
261
|
+
attribution: dict[str, Any] | None) -> None:
|
|
262
|
+
"""`record_failure` 的状态变更体(只改内存,不落盘)。
|
|
263
|
+
|
|
264
|
+
拆出来是为了让整段「重读 → 改 → 写」跑在同一个文件锁内:进程内
|
|
265
|
+
RLock 只挡同进程线程,CLI / MCP server / 评测脚本并行时各自持有
|
|
266
|
+
构造期读到的旧快照、各自 +1、后写者覆盖前写者。实测 4 进程 × 20 次
|
|
267
|
+
record_failure 丢 48%(6 进程丢 35%,8 进程丢 44%),丢掉的正是
|
|
268
|
+
`failures` / `opens` 增量——`DISABLE_AFTER_OPENS` 永远攒不够,
|
|
269
|
+
真坏掉的引擎于是持续被派发。这与 quota._mutate_locked 同一处理。
|
|
270
|
+
"""
|
|
271
|
+
with self._lock:
|
|
272
|
+
st = self._engines.get(engine) or {"failures": 0, "state": "closed"}
|
|
273
|
+
drives_opens = kind not in ("empty", "blocked", "rate-limited")
|
|
274
|
+
# empty 权重低:两次 empty 才算一次 failure 贡献
|
|
275
|
+
if kind == "empty":
|
|
276
|
+
st["empty_streak"] = int(st.get("empty_streak") or 0) + 1
|
|
277
|
+
if st["empty_streak"] < 2:
|
|
278
|
+
self._engines[engine] = st
|
|
279
|
+
return
|
|
280
|
+
st["empty_streak"] = 0
|
|
281
|
+
st["failures"] = int(st.get("failures") or 0) + 1
|
|
282
|
+
st["last_fail"] = time.time()
|
|
283
|
+
st["last_kind"] = kind
|
|
284
|
+
if attribution:
|
|
285
|
+
st["last_attribution"] = dict(attribution)
|
|
286
|
+
# empty(无结果)与 blocked(被拦截)都是查询级/源站级信号,不驱动
|
|
287
|
+
# open 熔断:否则「空结果→open 60s→half_open→再 open」无效 churn,
|
|
288
|
+
# 还占主位阻塞 6s。仅 error/timeout 驱动稳定 state 切换(空结果由
|
|
289
|
+
# 负缓存短 TTL 保底)。2026-08 修复;blocked 2026-09 并入同语义。
|
|
290
|
+
if drives_opens and (st["failures"] >= FAILURE_THRESHOLD
|
|
291
|
+
or st.get("state") == "half_open"):
|
|
292
|
+
st["state"] = "open"
|
|
293
|
+
st["opened_at"] = time.time()
|
|
294
|
+
# 连续 open 计数:仅 error/timeout 计入(引擎级故障),
|
|
295
|
+
# empty 不计入,避免「查询无结果」被误判为引擎持续故障
|
|
296
|
+
st["opens"] = int(st.get("opens") or 0) + 1
|
|
297
|
+
self._engines[engine] = st
|
|
298
|
+
|
|
299
|
+
def reenable(self, engine: str) -> None:
|
|
300
|
+
"""外部主动恢复(用户测试通过 / 新环境确认)。"""
|
|
301
|
+
|
|
302
|
+
def _m() -> None:
|
|
303
|
+
self._engines[engine] = {
|
|
304
|
+
"state": "closed", "failures": 0, "opens": 0,
|
|
305
|
+
"last_ok": time.time(), "reenabled_at": time.time(),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
self._mutate_locked(_m)
|
|
309
|
+
|
|
310
|
+
def record_note(self, engine: str,
|
|
311
|
+
attribution: dict[str, Any] | None = None) -> None:
|
|
312
|
+
"""只记录归因(「为什么不行」),完全不动熔断计数与状态。
|
|
313
|
+
|
|
314
|
+
用于配额耗尽这类**既不是引擎故障、也不该算失败**的场景:配额状态机
|
|
315
|
+
负责「停用多久」,这里只负责把原因留在可观测面上。此前 record_failure
|
|
316
|
+
是唯一写归因的入口,导致 quota-exhausted 分支为了让配额接管而丢弃
|
|
317
|
+
归因,博查 403 套餐额度不足永远显示 ready——P1 的原始动机场景。
|
|
318
|
+
"""
|
|
319
|
+
if not attribution:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
def _m() -> None:
|
|
323
|
+
st = self._engines.get(engine) or {"failures": 0, "state": "closed"}
|
|
324
|
+
st["last_attribution"] = dict(attribution)
|
|
325
|
+
self._engines[engine] = st
|
|
326
|
+
|
|
327
|
+
self._mutate_locked(_m)
|
|
328
|
+
|
|
329
|
+
def auto_disabled(self) -> list[str]:
|
|
330
|
+
"""返回所有处于自动禁用状态的引擎。"""
|
|
331
|
+
with self._lock:
|
|
332
|
+
self._fresh_locked()
|
|
333
|
+
return [e for e, st in self._engines.items()
|
|
334
|
+
if st.get("state") == "disabled"]
|
|
335
|
+
|
|
336
|
+
# ── 查询级负缓存 ────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _neg_key(query: str, engine: str) -> str:
|
|
340
|
+
raw = f"neg|{query}|{engine}"
|
|
341
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
|
342
|
+
|
|
343
|
+
def set_negative(self, query: str, engine: str, status: str = "no-results",
|
|
344
|
+
ttl: int | None = None) -> None:
|
|
345
|
+
ttl = ttl if ttl is not None else (
|
|
346
|
+
EMPTY_NEGATIVE_TTL if status == "no-results" else ERROR_NEGATIVE_TTL
|
|
347
|
+
)
|
|
348
|
+
key = self._neg_key(query, engine)
|
|
349
|
+
with self._lock:
|
|
350
|
+
self._neg[key] = {
|
|
351
|
+
"expires": time.time() + ttl,
|
|
352
|
+
"status": status,
|
|
353
|
+
"engine": engine,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
def get_negative(self, query: str, engine: str) -> Optional[dict[str, Any]]:
|
|
357
|
+
key = self._neg_key(query, engine)
|
|
358
|
+
with self._lock:
|
|
359
|
+
hit = self._neg.get(key)
|
|
360
|
+
if not hit:
|
|
361
|
+
return None
|
|
362
|
+
if time.time() >= float(hit.get("expires") or 0):
|
|
363
|
+
self._neg.pop(key, None)
|
|
364
|
+
return None
|
|
365
|
+
return hit
|
|
366
|
+
|
|
367
|
+
def clear_negative(self, query: str, engine: str) -> None:
|
|
368
|
+
key = self._neg_key(query, engine)
|
|
369
|
+
with self._lock:
|
|
370
|
+
self._neg.pop(key, None)
|
|
371
|
+
|
|
372
|
+
def stats(self) -> dict[str, Any]:
|
|
373
|
+
with self._lock:
|
|
374
|
+
open_engines = [e for e, s in self._engines.items() if s.get("state") == "open"]
|
|
375
|
+
return {
|
|
376
|
+
"open_engines": open_engines,
|
|
377
|
+
"tracked": len(self._engines),
|
|
378
|
+
"neg_entries": len(self._neg),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
_breaker: CircuitBreaker | None = None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def get_breaker() -> CircuitBreaker:
|
|
386
|
+
global _breaker
|
|
387
|
+
if _breaker is None:
|
|
388
|
+
_breaker = CircuitBreaker()
|
|
389
|
+
return _breaker
|