argo-search 1.0.1 → 2.9.7
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 +822 -0
- package/README.es.md +864 -0
- package/README.ja.md +864 -0
- package/README.ko.md +864 -0
- package/README.md +867 -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/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/assets/readme/why-better.svg +104 -0
- package/assets/readme/workflow.svg +83 -0
- package/backends/domain_profiles.json +2308 -96
- package/backends/engine_registry.yaml +3314 -503
- package/backends/query_synonyms_cn.json +35 -0
- package/backends/quota_profiles.json +2409 -161
- package/backends/source_types_cn.json +83 -0
- package/backends/text_lexicon.json +44 -0
- package/bin/argo +703 -0
- package/bin/argo.js +71 -39
- package/config.yaml +4976 -363
- 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 +564 -0
- package/docs/MCP_SETUP.md +106 -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/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/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/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 +31 -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/google_news.yaml +22 -0
- package/engines/specs/gov_regulations.yaml +38 -0
- package/engines/specs/guardian_rss.yaml +59 -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/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/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/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/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 +74 -0
- package/package.json +51 -12
- package/packages/dsh-plugin/README.md +134 -0
- package/packages/dsh-plugin/cordis.patch.yml +48 -0
- package/packages/dsh-plugin/dsh/client.js +1244 -0
- package/packages/dsh-plugin/dsh/index.js +2290 -0
- package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
- package/packages/dsh-plugin/package.json +38 -0
- package/packages/dsh-plugin/test/client.test.js +232 -0
- package/packages/dsh-plugin/test/config.test.js +30 -0
- package/packages/dsh-plugin/test/index.test.js +1338 -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 +271 -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/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/burstiness_detector.py +147 -0
- package/scripts/cache.py +1078 -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/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_security.py +685 -0
- package/scripts/content_signals.py +730 -0
- package/scripts/crawl.py +74 -35
- package/scripts/daemon.py +866 -0
- package/scripts/digup_bridge.py +214 -0
- package/scripts/engine_admission.py +366 -0
- package/scripts/engine_dispatch.py +1019 -0
- package/scripts/engine_env.py +534 -0
- package/scripts/engine_failure.py +411 -0
- package/scripts/engine_families.py +732 -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 +778 -294
- package/scripts/engines_base.py +1521 -0
- package/scripts/engines_builders.py +298 -0
- package/scripts/engines_builders_batch10.py +159 -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_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_search.py +440 -0
- package/scripts/engines_builders_tech.py +1360 -0
- package/scripts/evidence.py +588 -162
- package/scripts/evidence_loop.py +592 -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 +1779 -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 +184 -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 +0 -0
- package/scripts/image_vision/vision_probe.swift +126 -0
- package/scripts/install.ps1 +148 -0
- package/scripts/install.sh +206 -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_engines.py +321 -0
- package/scripts/local_image.py +970 -0
- package/scripts/local_seek.py +366 -0
- package/scripts/macro_countries.py +82 -0
- package/scripts/matrix_search_eval.py +1105 -0
- package/scripts/mcp_diag.py +8 -3
- package/scripts/mcp_handlers.py +1074 -0
- package/scripts/mcp_handlers_surface.py +157 -0
- package/scripts/mcp_launch.sh +122 -0
- package/scripts/mcp_server.py +118 -417
- package/scripts/mcp_setup.py +407 -0
- package/scripts/mcp_tools.py +330 -0
- package/scripts/mcp_transport.py +191 -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/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 +488 -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/redskill/redskill_engine.py +332 -0
- package/scripts/regression_p0p1.py +318 -0
- package/scripts/replay_eval.py +640 -0
- package/scripts/rerank_tfidf.py +225 -0
- package/scripts/research.py +591 -403
- package/scripts/research_cli.py +361 -0
- package/scripts/research_dossier.py +492 -0
- package/scripts/research_expand.py +146 -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/robots_guard.py +259 -0
- package/scripts/route.py +921 -254
- package/scripts/route_bangs.py +40 -0
- package/scripts/route_cache.py +225 -0
- package/scripts/route_combo.py +910 -0
- package/scripts/route_domains.py +328 -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 +897 -405
- package/scripts/search_attribution.py +88 -0
- package/scripts/search_benchmark.py +407 -0
- package/scripts/search_cli.py +627 -0
- package/scripts/search_entry.py +389 -0
- package/scripts/search_output.py +606 -0
- package/scripts/search_pipeline.py +1077 -0
- package/scripts/search_rank.py +991 -0
- package/scripts/search_types.py +27 -12
- package/scripts/seek_locator.py +68 -0
- package/scripts/semantic_evidence.py +215 -0
- package/scripts/serp_guard.py +228 -0
- package/scripts/serp_kill_log.py +51 -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 +95 -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 +402 -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/sub-skills/ego-search/SKILL.md +253 -0
- package/sub-skills/ego-search/references/browser-runtime.md +43 -0
- package/sub-skills/ego-search/references/install.md +67 -0
- package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -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/ego-search/tests/test_query_normalize.py +68 -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/js-run/tests/fixtures/challenge_like.js +27 -0
- package/sub-skills/js-run/tests/test_jsrun.py +165 -0
- package/sub-skills/local-search/SKILL.md +50 -53
- package/sub-skills/local-search/bm25.py +162 -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/search_v3.py +1182 -65
- 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/references/auto-behaviors.md +24 -0
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
- package/sub-skills/local-seek/references/git-integration.md +54 -0
- package/sub-skills/local-seek/references/remote-handoff.md +46 -0
- package/sub-skills/local-seek/references/rg-recipes.md +96 -0
- package/sub-skills/local-seek/references/strategies.md +52 -0
- package/sub-skills/local-seek/references/structural-search.md +59 -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 +193 -0
- package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
- package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
- package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
- package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
- package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
- package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""search_cli.py — 搜索的 CLI 入口(参数解析、人读输出、本地 seek 合并)。
|
|
3
|
+
|
|
4
|
+
从 search.py 拆出:那里是「执行流水线」(execute_search / super_search),这里是
|
|
5
|
+
「怎么被调用、结果怎么打印」。分开的实际收益是 `--help`/参数校验这类改动的验证
|
|
6
|
+
成本——它们不必跑整条流水线的测试,也不必读 2000 行的执行层。
|
|
7
|
+
|
|
8
|
+
入口不变:`python3 scripts/search.py "查询"`(search.py 的 __main__ 转到这里)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from cli_io import dumps, ensure_utf8_stdio
|
|
21
|
+
from engine_env import env_flag
|
|
22
|
+
from stage_timing import StageTiming
|
|
23
|
+
from time_utils import WINDOW_FORMATS_HINT, is_valid_time_window
|
|
24
|
+
from search import (
|
|
25
|
+
available_engines,
|
|
26
|
+
execute_search,
|
|
27
|
+
format_text_output,
|
|
28
|
+
format_timing,
|
|
29
|
+
super_search,
|
|
30
|
+
_IMPORTS_DONE,
|
|
31
|
+
_MODULE_T0,
|
|
32
|
+
_strip_for_agent,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# 真值时点的交接槽。由 search.py 的 __main__ 块在跑完整条 import 链后写入
|
|
37
|
+
# (见该文件底部),因为那一刻本模块的身份是 __main__,而 runpy 事后会把
|
|
38
|
+
# sys.modules['__main__'] 恢复成原模块——事后从 __main__ 上取不到了。
|
|
39
|
+
_TRUE_IMPORT_T0: float | None = None
|
|
40
|
+
_TRUE_IMPORTS_DONE: float | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resolve_module_timing() -> tuple[float, float]:
|
|
44
|
+
"""取 import 计时的两个时点。
|
|
45
|
+
|
|
46
|
+
三个来源按可靠性排序:
|
|
47
|
+
|
|
48
|
+
1. `_TRUE_IMPORT_T0` —— search.py 以 `__main__` 身份跑完整条 import 链
|
|
49
|
+
后写入的交接值,这是**真值**(约 30 ms)。
|
|
50
|
+
2. `sys.modules['__main__']` 上的同名属性 —— 直接 `python3 search.py` 时
|
|
51
|
+
成立。
|
|
52
|
+
3. 本模块 import 进来的 `search._MODULE_T0` —— **兜底,也是恒为 0 的那个**:
|
|
53
|
+
入口经 `bin/argo → runpy.run_module('search', run_name='__main__')`,
|
|
54
|
+
本模块执行 `from search import _MODULE_T0` 时,search 已以 __main__
|
|
55
|
+
跑过一遍,这是**第二次**导入,依赖全在 sys.modules 里,差值 ≈ 0。
|
|
56
|
+
|
|
57
|
+
走 3 正是 --explain-timing 的 import_ms 恒为 0.0 的原因,而它这一栏
|
|
58
|
+
恰恰是给「优化固定开销」用的——报 0 等于把优化者指向错误的数字。
|
|
59
|
+
"""
|
|
60
|
+
if _TRUE_IMPORT_T0 is not None and _TRUE_IMPORTS_DONE is not None:
|
|
61
|
+
return _TRUE_IMPORT_T0, _TRUE_IMPORTS_DONE
|
|
62
|
+
main_mod = sys.modules.get("__main__")
|
|
63
|
+
if main_mod is not None:
|
|
64
|
+
t0 = getattr(main_mod, "_MODULE_T0", None)
|
|
65
|
+
done = getattr(main_mod, "_IMPORTS_DONE", None)
|
|
66
|
+
if isinstance(t0, float) and isinstance(done, float):
|
|
67
|
+
return t0, done
|
|
68
|
+
return _MODULE_T0, _IMPORTS_DONE
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── CLI 主入口 ─────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _window_arg(value: str) -> str:
|
|
76
|
+
"""argparse 的 `type=`:坏时间窗直接拒(判据在 time_utils,提示语同源)。
|
|
77
|
+
|
|
78
|
+
用 `ArgumentTypeError` 而不是裸 `ValueError`:两者的差别是**用户看不看得见
|
|
79
|
+
原因**——argparse 对 ValueError 只印 "invalid _window_arg value: 'garbage'"
|
|
80
|
+
并把说明丢掉,对 ArgumentTypeError 则原样印出我们的提示。
|
|
81
|
+
"""
|
|
82
|
+
if not is_valid_time_window(value):
|
|
83
|
+
raise argparse.ArgumentTypeError(
|
|
84
|
+
f"无法解析的时间窗 {value!r}:{WINDOW_FORMATS_HINT}")
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _max_results_arg(value: str) -> int:
|
|
89
|
+
"""argparse 的 `type=`:条数必须为非负整数。
|
|
90
|
+
|
|
91
|
+
负数不是「取前 N 条」而是「从尾部砍掉 |N| 条」——`merged[: -3]` 在 10 条
|
|
92
|
+
结果上返回 7 条,比不传 -n 的默认 5 条还多,与「限制条数」的语义正好相反。
|
|
93
|
+
0 保留为合法(显式要空结果),只拒负数。
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
n = int(value)
|
|
97
|
+
except (TypeError, ValueError):
|
|
98
|
+
raise argparse.ArgumentTypeError(f"条数必须是整数,收到 {value!r}") from None
|
|
99
|
+
if n < 0:
|
|
100
|
+
raise argparse.ArgumentTypeError(
|
|
101
|
+
f"条数不能为负(收到 {n}):负数会从尾部截断,返回比不指定时更多的结果")
|
|
102
|
+
return n
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
106
|
+
"""构造 CLI 参数解析器。
|
|
107
|
+
|
|
108
|
+
从 main 里抽出来的唯一理由:让「CLI 是否支持某个已文档化的开关」可以
|
|
109
|
+
对着**解析器对象**断言,而不是 grep 源码文本——后者在重构(如把 CLI
|
|
110
|
+
搬到另一个模块)时会假红,且断言的是字面量而不是行为。
|
|
111
|
+
"""
|
|
112
|
+
parser = argparse.ArgumentParser(
|
|
113
|
+
description="Unified Search v2 — 统一搜索 CLI",
|
|
114
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
115
|
+
epilog="""
|
|
116
|
+
示例:
|
|
117
|
+
python3 search.py "python async"
|
|
118
|
+
python3 search.py "英伟达财报" --explain --json
|
|
119
|
+
python3 search.py "基金推荐" --mode fast
|
|
120
|
+
python3 search.py "AAPL" --engine anysearch --domain finance --sub_domain finance.us_stock
|
|
121
|
+
""",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument("query", nargs="?")
|
|
124
|
+
parser.add_argument("--engine", "-e", default="auto")
|
|
125
|
+
parser.add_argument("--max-results", "-n", type=_max_results_arg, default=5)
|
|
126
|
+
parser.add_argument("--depth", "-d", default="fast",
|
|
127
|
+
choices=["fast", "balanced", "deep"])
|
|
128
|
+
parser.add_argument("--academic-deep", action="store_true",
|
|
129
|
+
help="学术多源模式:自动设置 depth=deep + domain=academic,"
|
|
130
|
+
"禁用 early-stop 让所有学术源参与(arxiv/openalex/"
|
|
131
|
+
"local_pubmed/core 等),适合深度研究场景")
|
|
132
|
+
# --no-cache 与 ARGO_NO_CACHE 合并成一个判据。此前只有 flag:用户照文档
|
|
133
|
+
# 设 ARGO_NO_CACHE=1 却静默拿到缓存结果(cached=true、0 ms)——开关看着
|
|
134
|
+
# 生效、其实没生效,是最难查的一类问题。走 engine_env.env_flag 这一个
|
|
135
|
+
# 全仓统一的解析器,语义与其它 ARGO_* 开关一致。
|
|
136
|
+
parser.add_argument("--no-cache", action="store_true",
|
|
137
|
+
default=env_flag("ARGO_NO_CACHE", False))
|
|
138
|
+
parser.add_argument("--explain", action="store_true")
|
|
139
|
+
parser.add_argument("--json", action="store_true", dest="json_output")
|
|
140
|
+
parser.add_argument("--timeout", "-t", type=int, default=10)
|
|
141
|
+
parser.add_argument("--list-engines", action="store_true",
|
|
142
|
+
help="列出引擎;加 --detail 看 env/准入/routable 状态")
|
|
143
|
+
parser.add_argument("--detail", action="store_true",
|
|
144
|
+
help="与 --list-engines 联用:输出详细状态")
|
|
145
|
+
parser.add_argument("--routable-only", action="store_true",
|
|
146
|
+
help="与 --list-engines 联用:仅可自动路由的引擎")
|
|
147
|
+
parser.add_argument("--all", action="store_true", dest="list_all",
|
|
148
|
+
help="与 --list-engines --detail 联用:输出全部引擎的"
|
|
149
|
+
"逐条清单(默认只给分组摘要 + 非可路由项的成因)")
|
|
150
|
+
parser.add_argument("--mode", default="auto",
|
|
151
|
+
choices=["fast", "auto", "deep", "budget"],
|
|
152
|
+
help="预算模式: fast=免费优先, auto=成本感知, deep=质量优先, budget=配额控制")
|
|
153
|
+
parser.add_argument("--since", default=None,
|
|
154
|
+
type=_window_arg,
|
|
155
|
+
help="发布时间下限(7d / 2026-08-01),下推到支持时间窗的引擎")
|
|
156
|
+
parser.add_argument("--until", default=None,
|
|
157
|
+
type=_window_arg,
|
|
158
|
+
help="发布时间上限(7d / 2026-08-01),下推到支持时间窗的引擎")
|
|
159
|
+
parser.add_argument("--sort", default="relevance",
|
|
160
|
+
choices=["relevance", "oldest", "newest"],
|
|
161
|
+
help="时间排序:relevance=相关度(默认), oldest=最早在前(溯源), newest=最新在前")
|
|
162
|
+
parser.add_argument("--include-domains", default="",
|
|
163
|
+
help="仅保留这些域名(含子域),逗号分隔,如 github.com,arxiv.org")
|
|
164
|
+
parser.add_argument("--exclude-domains", default="",
|
|
165
|
+
help="排除这些域名(含子域),逗号分隔,如 pinterest.com")
|
|
166
|
+
parser.add_argument("--local-first", action="store_true",
|
|
167
|
+
help="强制优先使用 local_search 零成本聚合引擎")
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--include-local", action="store_true", default=None,
|
|
170
|
+
help="强制并入本机文件命中(source=local_files;fast/budget 模式默认已开)",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--no-local", action="store_true",
|
|
174
|
+
help="关闭本机文件命中(fast/budget 模式默认并入,此项退出)",
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument("--domain", default="", help="AnySearch 垂直域")
|
|
177
|
+
parser.add_argument("--sub_domain", default="", help="AnySearch 子域")
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--input-kind", default="auto",
|
|
180
|
+
choices=["auto", "keyword", "url-seed", "known-url"],
|
|
181
|
+
help="输入类型:known-url 默认不热搜;url-seed 只作发现线索",
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument("--plan-only", action="store_true",
|
|
184
|
+
help="仅输出离线计划(不联网)")
|
|
185
|
+
parser.add_argument("--force-search", action="store_true",
|
|
186
|
+
help="known-url 也强制多引擎搜索(不推荐)")
|
|
187
|
+
parser.add_argument("--envelope", action="store_true",
|
|
188
|
+
help="附加 candidates/sources/coverage(归档/来源追溯用;"
|
|
189
|
+
"--archive 自动开启)")
|
|
190
|
+
parser.add_argument("--no-envelope", action="store_true",
|
|
191
|
+
help="兼容保留:默认已不附加,此开关现在等同默认")
|
|
192
|
+
_tg = parser.add_mutually_exclusive_group()
|
|
193
|
+
_tg.add_argument(
|
|
194
|
+
"--timing", "--explain-timing", dest="timing",
|
|
195
|
+
action="store_true", default=True,
|
|
196
|
+
help="附加各阶段墙钟耗时(默认为开):stages 按耗时降序带占比、"
|
|
197
|
+
"dispatch 给出引擎并发效率,另附固定开销与进程总计。"
|
|
198
|
+
"想知道「这次搜索慢在哪」直接看它",
|
|
199
|
+
)
|
|
200
|
+
_tg.add_argument(
|
|
201
|
+
"--no-timing", dest="timing", action="store_false",
|
|
202
|
+
help="不附加阶段耗时(省几百字节上下文)",
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--fields", choices=("full", "agent"), default=None,
|
|
206
|
+
help="JSON 字段档位:full=全量;agent=只留答案内容(剥观测标量"
|
|
207
|
+
"与 null 键,每条 result 留 title/url/snippet/source/score 等;"
|
|
208
|
+
"fetch_required 保留)。默认 None=自动:stdout 非 tty 时"
|
|
209
|
+
"启用 agent 档(Agent 消费),否则 full",)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--archive",
|
|
212
|
+
action="store_true",
|
|
213
|
+
help="将本次搜索 envelope 落盘到工作区归档(不抓正文/不下载)",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--archive-dir",
|
|
217
|
+
type=str,
|
|
218
|
+
default=None,
|
|
219
|
+
help="归档根目录(默认 ARGO_ARCHIVE_ROOT 或 工作区/数据/argo-search-archive)",
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument("--archive-tag", default=None, help="归档标签,便于 list 过滤")
|
|
222
|
+
parser.add_argument("--archive-note", default=None, help="归档备注")
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"--verify",
|
|
225
|
+
nargs="?",
|
|
226
|
+
const=3,
|
|
227
|
+
type=int,
|
|
228
|
+
default=None,
|
|
229
|
+
metavar="TOP_K",
|
|
230
|
+
help="证据核验:对 top-k 未核验结果 fetch 正文、回填证据分、输出 evidence_revision 分布(默认 3)",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return parser
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ── --list-engines --detail 无过滤时的摘要(2026-09-27)────────────────────────
|
|
237
|
+
#
|
|
238
|
+
# 逐条清单要回答的问题只有两个:**有多少源可用**、**不可用的那些为什么不可用**。
|
|
239
|
+
# 245 个健康源逐条列出对这两个问题的贡献是零(每一条的答案都是「可用」),
|
|
240
|
+
# 而代价是 54 KB ≈ 14k token 进上下文。所以默认给分组计数 + 只列非可路由项。
|
|
241
|
+
|
|
242
|
+
# 不可自动路由的成因,顺序即优先级:一个源可能同时缺 env 又缺依赖,若把每种
|
|
243
|
+
# 成立的理由都计一次,分组之和会大于「不可路由总数」,自相矛盾。只认第一个。
|
|
244
|
+
_ENGINE_BLOCK_REASONS: tuple[tuple[str, str], ...] = (
|
|
245
|
+
("disabled", "enabled=False(未启用)"),
|
|
246
|
+
("missing_env", "缺环境变量(配好密钥即可用)"),
|
|
247
|
+
("missing_deps", "缺可选依赖(装好即可用)"),
|
|
248
|
+
("quota_exhausted", "配额耗尽(等窗口重置)"),
|
|
249
|
+
("not_routable", "其它不可路由(见 --engine <名> 全量行)"),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _engine_block_reason(row: dict[str, Any]) -> str | None:
|
|
254
|
+
"""一条引擎记录「不可自动路由」的成因;可自动路由返回 None。"""
|
|
255
|
+
if not row.get("enabled", True):
|
|
256
|
+
return "disabled"
|
|
257
|
+
if row.get("missing_env"):
|
|
258
|
+
return "missing_env"
|
|
259
|
+
if row.get("missing_deps"):
|
|
260
|
+
return "missing_deps"
|
|
261
|
+
if row.get("quota_exhausted"):
|
|
262
|
+
return "quota_exhausted"
|
|
263
|
+
if not row.get("routable", True):
|
|
264
|
+
return "not_routable"
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _summarize_engine_rows(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
|
269
|
+
"""把逐条清单压成「计数 + 只列非可路由项」。"""
|
|
270
|
+
routable: list[str] = []
|
|
271
|
+
explicit_only: list[str] = []
|
|
272
|
+
blocked: list[dict[str, Any]] = []
|
|
273
|
+
by_reason: dict[str, int] = {}
|
|
274
|
+
for row in rows:
|
|
275
|
+
name = str(row.get("engine_id") or "")
|
|
276
|
+
if row.get("explicit_only"):
|
|
277
|
+
# 只能显式点名、不进自动路由——这是设计如此,不是故障,
|
|
278
|
+
# 所以单独一桶,不混进「不可用」,也不逐条展开。
|
|
279
|
+
explicit_only.append(name)
|
|
280
|
+
continue
|
|
281
|
+
reason = _engine_block_reason(row)
|
|
282
|
+
if reason is None:
|
|
283
|
+
routable.append(name)
|
|
284
|
+
continue
|
|
285
|
+
by_reason[reason] = by_reason.get(reason, 0) + 1
|
|
286
|
+
blocked.append({
|
|
287
|
+
"engine_id": name,
|
|
288
|
+
"reason": reason,
|
|
289
|
+
**({"missing_env": row["missing_env"]} if row.get("missing_env") else {}),
|
|
290
|
+
**({"missing_deps": row["missing_deps"]} if row.get("missing_deps") else {}),
|
|
291
|
+
**({"status": row["status"]} if row.get("status") else {}),
|
|
292
|
+
})
|
|
293
|
+
return {
|
|
294
|
+
"total": len(rows),
|
|
295
|
+
"routable": len(routable),
|
|
296
|
+
"explicit_only": len(explicit_only),
|
|
297
|
+
"not_routable": len(blocked),
|
|
298
|
+
"by_reason": {k: by_reason[k] for k, _ in _ENGINE_BLOCK_REASONS
|
|
299
|
+
if k in by_reason},
|
|
300
|
+
"reason_legend": {k: label for k, label in _ENGINE_BLOCK_REASONS},
|
|
301
|
+
"not_routable_engines": sorted(blocked, key=lambda r: r["reason"]),
|
|
302
|
+
"hint": "逐条全量清单加 --all;单引擎全量诊断用 --engine <名>",
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _format_engine_summary(summary: dict[str, Any]) -> str:
|
|
307
|
+
"""摘要的人类可读渲染(非 --json 时用)。"""
|
|
308
|
+
lines = [
|
|
309
|
+
f"引擎清单摘要:共 {summary['total']} 条,可自动路由 {summary['routable']} 条,"
|
|
310
|
+
f"仅显式点名 {summary['explicit_only']} 条,不可路由 {summary['not_routable']} 条",
|
|
311
|
+
]
|
|
312
|
+
for reason, count in (summary.get("by_reason") or {}).items():
|
|
313
|
+
label = (summary.get("reason_legend") or {}).get(reason, reason)
|
|
314
|
+
lines.append(f" · {reason:<16}{count:>4} {label}")
|
|
315
|
+
blocked = summary.get("not_routable_engines") or []
|
|
316
|
+
if blocked:
|
|
317
|
+
lines.append("不可路由项:")
|
|
318
|
+
for row in blocked:
|
|
319
|
+
extra = ""
|
|
320
|
+
if row.get("missing_env"):
|
|
321
|
+
# 成因与细节可能同时成立(如「未启用」的源也缺密钥):
|
|
322
|
+
# 用「另缺」而不是直接并列,避免读成两个互斥的原因。
|
|
323
|
+
extra = f"(另缺 {'/'.join(str(x) for x in row['missing_env'])})"
|
|
324
|
+
elif row.get("missing_deps"):
|
|
325
|
+
extra = f"(另缺 {'/'.join(str(x) for x in row['missing_deps'])})"
|
|
326
|
+
lines.append(f" - {row['engine_id']:<28}{row['reason']}{extra}")
|
|
327
|
+
lines.append(str(summary.get("hint") or ""))
|
|
328
|
+
return "\n".join(lines)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def main():
|
|
332
|
+
# 入口第一件事:钉住 stdout/stderr 编码(实现唯一,见 cli_io)。
|
|
333
|
+
# 放在这里而不是 import 期:本模块被 search.py 与 bin/argo 两条路调用,
|
|
334
|
+
# 入口点是唯一能覆盖两者的位置。
|
|
335
|
+
ensure_utf8_stdio()
|
|
336
|
+
parser = build_parser()
|
|
337
|
+
args = parser.parse_args()
|
|
338
|
+
|
|
339
|
+
if args.list_engines:
|
|
340
|
+
# --engine 兼作清单过滤(`--engine auto` 是搜索默认值,不算过滤)。
|
|
341
|
+
# 全量详细行 2026-09-16 实测 151 KB(runtime/admission 嵌套是大头),
|
|
342
|
+
# Agent 查单个引擎状态时不该付这个代价——实测此前
|
|
343
|
+
# `--list-engines --detail --engine egov_law` 忽略过滤、照样吐全量。
|
|
344
|
+
# 现无过滤时走 compact_engine_row 瘦身(~1/4 体积),有过滤给全量。
|
|
345
|
+
_wanted = None
|
|
346
|
+
if args.engine and args.engine != "auto":
|
|
347
|
+
_wanted = [e.strip() for e in args.engine.split(",") if e.strip()] or None
|
|
348
|
+
if args.detail:
|
|
349
|
+
try:
|
|
350
|
+
from engine_status import (list_engines_detail,
|
|
351
|
+
compact_engine_row,
|
|
352
|
+
format_engines_table)
|
|
353
|
+
rows = list_engines_detail(routable_only=args.routable_only,
|
|
354
|
+
engines=_wanted)
|
|
355
|
+
if not _wanted:
|
|
356
|
+
# 不带过滤时**默认不吐逐条清单**(2026-09-27 收紧)。
|
|
357
|
+
#
|
|
358
|
+
# 沿革:全量转储 151 KB → 瘦身投影 54 KB(compact_engine_row,
|
|
359
|
+
# 2026-09-16)→ 仍在一次调用里塞 54 KB ≈ 14k token 进上下文。
|
|
360
|
+
# 而这一档要回答的问题只有两个:**有多少源可用**、**不可用的
|
|
361
|
+
# 那些为什么不可用**。逐条列出 245 个健康源,是对这两个问题
|
|
362
|
+
# 的零贡献——它们每一个的答案都是「可用」。
|
|
363
|
+
#
|
|
364
|
+
# 所以默认改成「分组计数 + 只列非可路由项(带成因)」,
|
|
365
|
+
# 实测 54 KB → 约 2 KB;要逐条全量照旧加 --all。
|
|
366
|
+
# 单引擎全量诊断仍走 --engine <名>(不受本分支影响)。
|
|
367
|
+
rows = [compact_engine_row(r) for r in rows]
|
|
368
|
+
if not _wanted and not args.list_all and args.json_output:
|
|
369
|
+
print(dumps(_summarize_engine_rows(rows)))
|
|
370
|
+
return
|
|
371
|
+
if not _wanted and not args.list_all:
|
|
372
|
+
print(_format_engine_summary(_summarize_engine_rows(rows)))
|
|
373
|
+
return
|
|
374
|
+
if _wanted:
|
|
375
|
+
# 未命中的名字要显式报出(走 stderr,保持 stdout 是纯 JSON)——
|
|
376
|
+
# 否则「查了没输出」会被误读成「该引擎状态为空」。
|
|
377
|
+
_got = {r.get("engine_id") for r in rows}
|
|
378
|
+
_missing = [e for e in _wanted if e not in _got]
|
|
379
|
+
if _missing:
|
|
380
|
+
print(f"[list-engines] 未收录的引擎: {', '.join(_missing)}",
|
|
381
|
+
file=sys.stderr)
|
|
382
|
+
if args.json_output:
|
|
383
|
+
print(dumps(rows))
|
|
384
|
+
else:
|
|
385
|
+
print(format_engines_table(rows))
|
|
386
|
+
except Exception as e:
|
|
387
|
+
print(dumps({"error": str(e), "engines": available_engines()}))
|
|
388
|
+
else:
|
|
389
|
+
# 不包 try/except TypeError:available_engines 已收 routable_only
|
|
390
|
+
# (它当初没收,被这里的回退静默吞掉,于是 --routable-only 一直
|
|
391
|
+
# 返回全量)。契约见 test_engine_catalog 的签名用例。
|
|
392
|
+
names = available_engines(routable_only=args.routable_only)
|
|
393
|
+
if _wanted:
|
|
394
|
+
names = [n for n in names if n in set(_wanted)]
|
|
395
|
+
print(dumps(names))
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# 入口级引擎名校验(2026-10-10 方案甲):无效名字此前只在执行层补 errors,
|
|
399
|
+
# status 仍是 completed、--plan-only 连 errors 都没有——「失败伪装成空结果」
|
|
400
|
+
# (09-20 实踩、10-10 复测仍在,与 #12 静默失效同类)。判据单点复用
|
|
401
|
+
# unknown_requested_engines;did-you-mean 给最近名。只在显式点名时付
|
|
402
|
+
# engines 模块的导入成本(~115ms),auto 默认零开销。
|
|
403
|
+
if args.engine and args.engine != "auto":
|
|
404
|
+
from engines import unknown_requested_engines
|
|
405
|
+
_unknown = unknown_requested_engines(args.engine)
|
|
406
|
+
if _unknown:
|
|
407
|
+
import difflib
|
|
408
|
+
# 别名导入:main 里 list 分支的 except 也引用 available_engines,
|
|
409
|
+
# 同名局部导入会把它遮成 UnboundLocalError(test_local_import_
|
|
410
|
+
# shadows_global 同款),必须换名。
|
|
411
|
+
from engines import available_engines as _avail_engines
|
|
412
|
+
_catalog = _avail_engines()
|
|
413
|
+
_hint = "; ".join(
|
|
414
|
+
f"{u}(最接近: {', '.join(difflib.get_close_matches(u, _catalog, n=3, cutoff=0.4)) or '无'})"
|
|
415
|
+
for u in _unknown)
|
|
416
|
+
parser.error(f"未知引擎: {_hint} —— 全量名单见 --list-engines")
|
|
417
|
+
|
|
418
|
+
if not args.query:
|
|
419
|
+
parser.error("必须提供搜索关键词")
|
|
420
|
+
|
|
421
|
+
# envelope 默认**关**,要时显式开(--envelope / --archive)。
|
|
422
|
+
#
|
|
423
|
+
# why(2026-09-17 实测):默认附 envelope 时一次 5 条结果的 JSON 输出 15454 B,
|
|
424
|
+
# 其中 14117 B(82%)是同一批结果的三视图重复——snippet 被写三遍(5701 B,
|
|
425
|
+
# 占全文 40%),candidates 每条还重复一遍 query。而这三个视图的角色是
|
|
426
|
+
# **归档与来源追溯**:归档路径(--archive)本来就会把 candidates 落成
|
|
427
|
+
# candidates.jsonl、并在缺 sources 时从 results 回填,所以「归档要全量」
|
|
428
|
+
# 由 `or args.archive` 这一支保证,默认翻转**不损失任何能力**——需要
|
|
429
|
+
# provenance 的调用者加 --envelope 即可拿到与从前逐字节相同的文档。
|
|
430
|
+
#
|
|
431
|
+
# 之前的方向是「默认给全量、调用者自己记得减」(--no-envelope),
|
|
432
|
+
# 于是忘记加开关的调用者每次多付约 2.4k token。默认档应当服务最常见的
|
|
433
|
+
# 那个用途,而不是服务最重的那个用途。
|
|
434
|
+
use_envelope = args.envelope or args.archive
|
|
435
|
+
# 阶段耗时:**默认开**。它是「这次慢在哪」的自解释入口——不开的话,
|
|
436
|
+
# 想知道瓶颈只能外部计时 + 临时代码,等于把优化门槛抬到只有维护者能过。
|
|
437
|
+
_timing = StageTiming() if args.timing else None
|
|
438
|
+
|
|
439
|
+
# 路由预热(`_prewarm`)**已删除**(2026-09-27)。历史:daemon 线程版 →
|
|
440
|
+
# 同步版(「把 22ms 变成确定的支出」)→ 删除。
|
|
441
|
+
#
|
|
442
|
+
# 它建立在「预热的那笔钱 route 一定要付」这个前提上,而实测前提不成立:
|
|
443
|
+
#
|
|
444
|
+
# 1. **route-cache 命中时 route 根本不编译域正则**(生产默认,覆盖几乎
|
|
445
|
+
# 所有重复查询)。交替 5 组实测:route 阶段在有无预热下都是 4.2ms,
|
|
446
|
+
# 而整次调用的中位墙上时间 107.8ms → 68.9ms——预热是 100% 白付,
|
|
447
|
+
# 省下的 38.9ms(36%)正是这笔支出本身。
|
|
448
|
+
# 2. **未命中时也预付错了对象**。profile 显示 route_query 首次调用的
|
|
449
|
+
# 开销主要来自懒加载 import 与构建 249 个引擎的注册表;先预热再
|
|
450
|
+
# `route_query`,route 阶段实测仍要 47–121ms。域编译只是其中一小截。
|
|
451
|
+
# 3. 未命中路径的总额不因删除而变:编译挪回 route 内部,同样只付一次。
|
|
452
|
+
#
|
|
453
|
+
# 想重新引入时先读这三条:任何「预热」都必须证明它预热的正是 route 会在
|
|
454
|
+
# 同一进程、同一路径上付的那一笔,否则只是把支出提前,不是省下。
|
|
455
|
+
# 回归守卫见 tests/test_plan_a_optimizations.py::TestNoUnconditionalPrewarm。
|
|
456
|
+
# --academic-deep:学术多源模式,自动设置 depth=deep + domain=academic
|
|
457
|
+
if args.academic_deep:
|
|
458
|
+
depth = "deep"
|
|
459
|
+
engine_domain = "academic"
|
|
460
|
+
else:
|
|
461
|
+
depth = args.depth
|
|
462
|
+
engine_domain = args.domain or None
|
|
463
|
+
|
|
464
|
+
results = super_search(
|
|
465
|
+
query=args.query,
|
|
466
|
+
engine=args.engine,
|
|
467
|
+
n=args.max_results,
|
|
468
|
+
explain=args.explain,
|
|
469
|
+
skip_cache=args.no_cache,
|
|
470
|
+
timeout=args.timeout,
|
|
471
|
+
depth=depth,
|
|
472
|
+
mode=args.mode,
|
|
473
|
+
local_first=args.local_first,
|
|
474
|
+
input_kind=args.input_kind,
|
|
475
|
+
plan_only=args.plan_only,
|
|
476
|
+
force_search=args.force_search,
|
|
477
|
+
envelope=use_envelope,
|
|
478
|
+
since=args.since,
|
|
479
|
+
until=args.until,
|
|
480
|
+
sort=args.sort,
|
|
481
|
+
include_domains=[d for d in args.include_domains.split(",") if d.strip()] or None,
|
|
482
|
+
exclude_domains=[d for d in args.exclude_domains.split(",") if d.strip()] or None,
|
|
483
|
+
engine_domain=engine_domain,
|
|
484
|
+
engine_sub_domain=args.sub_domain or None,
|
|
485
|
+
timing=_timing,
|
|
486
|
+
# 三态:--include-local 强制开 / --no-local 强制关 / 都不给 = None
|
|
487
|
+
#(由 super_search 按模式自动解析,fast/budget 开)。本地命中并入
|
|
488
|
+
# 统一走 shape_response(search_output)——此前 CLI 在搜索结束后
|
|
489
|
+
# 串行补一次合并(白等一个 seek 子进程,且与库路径双份逻辑),
|
|
490
|
+
# 交回 super_search 后自动获得 L1 并行提交(与主搜索同时跑)。
|
|
491
|
+
include_local=(True if args.include_local
|
|
492
|
+
else (False if args.no_local else None)),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# 固定开销(import + argparse + 收尾):缓存命中时它占墙钟大头,而它不出现
|
|
496
|
+
# 在任何阶段里——不显式报出来,看的人会把「启动 80 ms」当成「搜索 80 ms」,
|
|
497
|
+
# 优化方向就找错了。
|
|
498
|
+
if _timing is not None and "timing" in results:
|
|
499
|
+
_mod_t0, _imports_done = _resolve_module_timing()
|
|
500
|
+
_now_ms = (time.perf_counter() - _mod_t0) * 1000.0
|
|
501
|
+
_stages = results["timing"].get("stages_ms") or 0
|
|
502
|
+
results["timing"]["import_ms"] = round(
|
|
503
|
+
(_imports_done - _mod_t0) * 1000.0, 1)
|
|
504
|
+
results["timing"]["overhead_ms"] = round(max(0.0, _now_ms - _stages), 1)
|
|
505
|
+
results["timing"]["process_ms"] = round(_now_ms, 1)
|
|
506
|
+
|
|
507
|
+
# 本地命中并入已上移进 super_search(shape_response 统一出口,L1 并行),
|
|
508
|
+
# 这里不再串行补合并——双份合并会让同一批命中出现两次。
|
|
509
|
+
|
|
510
|
+
if args.archive and results.get("status") != "handoff_required":
|
|
511
|
+
try:
|
|
512
|
+
from archive_run import write_search_archive, resolve_archive_root
|
|
513
|
+
root = resolve_archive_root(args.archive_dir) if args.archive_dir else None
|
|
514
|
+
if args.archive_dir:
|
|
515
|
+
root = resolve_archive_root(args.archive_dir)
|
|
516
|
+
meta = write_search_archive(
|
|
517
|
+
results,
|
|
518
|
+
root=root,
|
|
519
|
+
tag=args.archive_tag,
|
|
520
|
+
note=args.archive_note,
|
|
521
|
+
source="argo_search",
|
|
522
|
+
)
|
|
523
|
+
if not args.json_output:
|
|
524
|
+
print(
|
|
525
|
+
f" [archive] {meta.get('run_id')} → {meta.get('run_dir')}",
|
|
526
|
+
file=sys.stderr,
|
|
527
|
+
)
|
|
528
|
+
except Exception as e:
|
|
529
|
+
print(f" [archive error] {type(e).__name__}: {e}", file=sys.stderr)
|
|
530
|
+
|
|
531
|
+
# 证据完整链路 P0:--verify 显式核验 top-k 未核验结果(fetch + 回填 + revision 分布)
|
|
532
|
+
_t_verify0 = time.perf_counter()
|
|
533
|
+
if args.verify:
|
|
534
|
+
try:
|
|
535
|
+
from evidence_loop import verify_results, reorder_by_evidence
|
|
536
|
+
v = verify_results(results.get("results") or [], args.query, top_k=args.verify)
|
|
537
|
+
results["verify"] = v
|
|
538
|
+
# A-3(2026-09-27):正文级证据**回写排序**。此前 verify 只改字段不重排,
|
|
539
|
+
# 抓回的正文质量到不了排序器——花了 RTT 却不改结果次序。
|
|
540
|
+
try:
|
|
541
|
+
reorder = reorder_by_evidence(results.get("results") or [])
|
|
542
|
+
if reorder.get("reordered"):
|
|
543
|
+
results["evidence_reorder"] = reorder
|
|
544
|
+
except Exception as _re:
|
|
545
|
+
print(f" [verify reorder skipped] {type(_re).__name__}", file=sys.stderr)
|
|
546
|
+
results["fetch_required"] = bool(results.get("fetch_required"))
|
|
547
|
+
# verify 已回填/核验结果 → 刷新门控汇总,避免 suggested 含已核验 URL
|
|
548
|
+
try:
|
|
549
|
+
from evidence_loop import gate_results
|
|
550
|
+
gate = gate_results(results.get("results") or [], results.get("domain"))
|
|
551
|
+
results["evidence_loop"] = {
|
|
552
|
+
"high_consequence_domain": gate["high_consequence_domain"],
|
|
553
|
+
"suggested": gate["suggested"],
|
|
554
|
+
"verified_count": gate["verified_count"],
|
|
555
|
+
"pending_count": gate["pending_count"],
|
|
556
|
+
}
|
|
557
|
+
except Exception as _ge:
|
|
558
|
+
# 内层 pass 会把 evidence_loop 从 --json 输出里静默抹掉,
|
|
559
|
+
# 调用方无从得知门控汇总已缺失。与上方 verify reorder 同款:
|
|
560
|
+
# 打到 stderr,不打断主流程。
|
|
561
|
+
print(f" [evidence_loop refresh skipped] {type(_ge).__name__}",
|
|
562
|
+
file=sys.stderr)
|
|
563
|
+
if not args.json_output:
|
|
564
|
+
rs = v.get("revision_summary") or {}
|
|
565
|
+
print(
|
|
566
|
+
f" [verify] 核验 {rs.get('n', 0)} 条,"
|
|
567
|
+
f"improved={rs.get('improved', 0)} unchanged={rs.get('unchanged', 0)} "
|
|
568
|
+
f"degraded={rs.get('degraded', 0)} mean_delta={rs.get('mean_delta', 0)}",
|
|
569
|
+
file=sys.stderr,
|
|
570
|
+
)
|
|
571
|
+
ro = results.get("evidence_reorder") or {}
|
|
572
|
+
if ro.get("reordered"):
|
|
573
|
+
print(f" [verify] 证据回写排序:调整 {len(ro.get('adjusted') or [])} 条,"
|
|
574
|
+
f"位次变动 {ro.get('moved', 0)} 处", file=sys.stderr)
|
|
575
|
+
except Exception as e:
|
|
576
|
+
print(f" [verify error] {type(e).__name__}: {e}", file=sys.stderr)
|
|
577
|
+
|
|
578
|
+
# 计时盲区修补(2026-09-29):--verify 的 top-k fetch 发生在上面计时快照
|
|
579
|
+
# (process_ms 定格处)之后,净增 280–1160ms 完全不进任何账——实测
|
|
580
|
+
# wall 776–1746ms vs process_ms 494–701ms,「这次为什么慢」归因指错
|
|
581
|
+
# 方向。快照后移做不到(verify 是装配后的可选后处理),补一个 stage
|
|
582
|
+
# 并把耗时并回 stages_ms / process_ms,两个总数保持自洽。
|
|
583
|
+
if _timing is not None and "timing" in results and args.verify:
|
|
584
|
+
_verify_ms = (time.perf_counter() - _t_verify0) * 1000.0
|
|
585
|
+
_t = results["timing"]
|
|
586
|
+
_total = float(_t.get("stages_ms") or 0) + _verify_ms
|
|
587
|
+
_t.setdefault("stages", []).append({
|
|
588
|
+
"stage": "verify",
|
|
589
|
+
"ms": round(_verify_ms, 1),
|
|
590
|
+
"pct": round(_verify_ms * 100.0 / _total, 1) if _total > 0 else 0.0,
|
|
591
|
+
})
|
|
592
|
+
_t["stages_ms"] = round(_total, 1)
|
|
593
|
+
_t["process_ms"] = round(
|
|
594
|
+
float(_t.get("process_ms") or 0) + _verify_ms, 1)
|
|
595
|
+
|
|
596
|
+
if args.json_output:
|
|
597
|
+
public = {k: v for k, v in results.items() if not k.startswith("_")}
|
|
598
|
+
# Agent 消费自动瘦身:stdout 非 tty(管道/Agent 调用)且未显式指定
|
|
599
|
+
# --fields 时,自动启用 agent 档。实测输出从 15.4KB → 4.3KB
|
|
600
|
+
# (-72%),token 消耗从 ~3863 → ~1086。显式 --fields full/agent 可覆盖。
|
|
601
|
+
from cli_io import stdout_is_tty
|
|
602
|
+
_auto_agent = (
|
|
603
|
+
not stdout_is_tty()
|
|
604
|
+
and args.fields is None
|
|
605
|
+
and not args.envelope
|
|
606
|
+
)
|
|
607
|
+
if args.fields == "agent" or _auto_agent:
|
|
608
|
+
if args.envelope:
|
|
609
|
+
print(
|
|
610
|
+
" [warning] --envelope 与 --fields agent 同时给时 envelope 会被"
|
|
611
|
+
"剥光(实测增量 0 字节):要 provenance 请去掉 --fields agent;"
|
|
612
|
+
"只要瘦身就别加 --envelope。",
|
|
613
|
+
file=sys.stderr,
|
|
614
|
+
)
|
|
615
|
+
public = _strip_for_agent(public)
|
|
616
|
+
print(dumps(public))
|
|
617
|
+
else:
|
|
618
|
+
print(format_text_output(results))
|
|
619
|
+
if results.get("timing"):
|
|
620
|
+
print(format_timing(results["timing"]))
|
|
621
|
+
if results.get("archive"):
|
|
622
|
+
ar = results["archive"]
|
|
623
|
+
print(f" archived → {ar.get('run_dir')}")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
if __name__ == "__main__":
|
|
627
|
+
main()
|