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,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""query_signals.py — 查询改写与「结果够不够」的判定。
|
|
3
|
+
|
|
4
|
+
从 search.py 中外提。这里放的全是**本地启发式**判断,不发起网络请求、
|
|
5
|
+
不依赖搜索编排状态,因此可以被搜索之外的层(评测、路由、批量任务)复用。
|
|
6
|
+
|
|
7
|
+
两类信号:
|
|
8
|
+
- 查询改写(apply_query_rewrite):把口语化查询换成更适合检索的形式
|
|
9
|
+
- 早停判定(results_sufficient / cumulative_sufficient / query_coverage_ok):
|
|
10
|
+
结果是否已经够用、可以不再等慢源
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def apply_query_rewrite(query: str) -> tuple[str, dict | None]:
|
|
19
|
+
"""统一查询改写逻辑,返回 (改写后的查询, 改写结果字典)。
|
|
20
|
+
|
|
21
|
+
改写失败时静默返回原查询,不影响搜索流程。
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
from query_rewriter import rewrite_query as do_rewrite
|
|
25
|
+
result = do_rewrite(query)
|
|
26
|
+
if result["rewritten"] and result["confidence"] >= 0.7:
|
|
27
|
+
return result["rewritten"], result
|
|
28
|
+
except ImportError:
|
|
29
|
+
pass # query_rewriter 模块不可用,使用原查询
|
|
30
|
+
except Exception as e:
|
|
31
|
+
import logging
|
|
32
|
+
logging.getLogger("unified_search").debug(f"查询改写跳过: {type(e).__name__}")
|
|
33
|
+
return query, None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def results_sufficient(
|
|
37
|
+
results: list[dict[str, Any]],
|
|
38
|
+
mode: str = "auto",
|
|
39
|
+
min_results: int | None = None,
|
|
40
|
+
query: str = "",
|
|
41
|
+
) -> bool:
|
|
42
|
+
return _sufficient_internal(results, mode, min_results, query)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cumulative_sufficient(raw_results: dict[str, list[dict[str, Any]]],
|
|
46
|
+
mode: str = "auto",
|
|
47
|
+
min_results: int | None = None,
|
|
48
|
+
query: str = "") -> bool:
|
|
49
|
+
"""跨引擎累计结果是否已够(wave-2 提前终止判定)。
|
|
50
|
+
|
|
51
|
+
与 results_sufficient 同阈值,但把 raw_results 里所有已完成的
|
|
52
|
+
引擎结果合并成一条列表再判,避免单一引擎不足时重复等待慢源。
|
|
53
|
+
壳链不算「够数」的口径在 _sufficient_internal 统一定义
|
|
54
|
+
(2026-10-11 热榜实测,判定与下游 serp_guard 过滤对齐)。
|
|
55
|
+
"""
|
|
56
|
+
merged: list[dict[str, Any]] = []
|
|
57
|
+
for res in raw_results.values():
|
|
58
|
+
if not res:
|
|
59
|
+
continue
|
|
60
|
+
merged.extend(
|
|
61
|
+
r for r in res if isinstance(r, dict) and "error" not in r
|
|
62
|
+
)
|
|
63
|
+
return _sufficient_internal(merged, mode, min_results, query)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_query_echo(results: list[dict[str, Any]], query: str) -> bool:
|
|
67
|
+
"""这批结果是否是「查询词回声」——上游把整句当单词去查,返回查询中
|
|
68
|
+
**某一个词**的词典释义/同主题页。
|
|
69
|
+
|
|
70
|
+
2026-10-17 实锤:「keyboard switch lube service 2026」全批命中
|
|
71
|
+
"Online Virtual Keyboard"、「best mechanical keyboard for typing 2026」
|
|
72
|
+
全批命中 "BEST 的词典释义"——整批标题都在回声**同一个**查询词。
|
|
73
|
+
|
|
74
|
+
判据只看标题:snippet 里的功能词(for/the/and…)会把单条覆盖抬到 2,
|
|
75
|
+
判据即被击穿——实测 "Shop Best Buy for electronics" 仅因含 for 就
|
|
76
|
+
cov=2,而标题侧仍只有 best 一个词。标题是结果的核心,功能词出现率低。
|
|
77
|
+
|
|
78
|
+
只判整批、不判单条:长尾查询本就有弱命中,单条覆盖 1 词完全正常;
|
|
79
|
+
整批都只覆盖同一个词才是「上游降级」的信号。零网络成本(只读文本与
|
|
80
|
+
查询词元),可在早停(rerank 之前,rel 尚不存在)与输出层(恢复链)
|
|
81
|
+
复用——这与 cache_guard.is_degraded_results 的「整批退化」同构,区别是
|
|
82
|
+
那里读 rerank 的 relevance,这里读词面。
|
|
83
|
+
"""
|
|
84
|
+
if not query or not query.strip() or not results:
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
from tfidf_router import tokenize
|
|
88
|
+
except ImportError:
|
|
89
|
+
return False # 分词不可用不判(fail-open)
|
|
90
|
+
q_set = set(tokenize(query))
|
|
91
|
+
# 查询词 <4 不判:短查询的结果标题天然只命中一两个词,拦它即误伤(实测
|
|
92
|
+
# 「OpenAI MCP specification」的专利/论文结果只命中 specification 一个词,
|
|
93
|
+
# 却被判回声 → 恢复链误触发,白打两个引擎还把金标输出撑大)。
|
|
94
|
+
if len(q_set) < 4:
|
|
95
|
+
return False
|
|
96
|
+
# 至少 3 条结果才判:一两条短标题完全可能是正确答案(实测
|
|
97
|
+
# 「what is the capital of France」→ "Paris capital" 只命中 capital 一个词,
|
|
98
|
+
# 但那就是答案)。
|
|
99
|
+
if len(results) < 3:
|
|
100
|
+
return False
|
|
101
|
+
covered_terms: set[str] = set()
|
|
102
|
+
for r in results:
|
|
103
|
+
if isinstance(r, dict):
|
|
104
|
+
covered_terms |= (q_set & set(tokenize(r.get("title") or "")))
|
|
105
|
+
# 判据取「整批标题覆盖的**不同**查询词数恰好为 1」:
|
|
106
|
+
# 0 个 = 完全无关,归 covered 判据管(算进来会让离线 mock 误触发恢复链,
|
|
107
|
+
# 实测 test_funnel 的 called 从 2 涨到 4);
|
|
108
|
+
# 1 个 = 整批都在回声同一个词("Online Virtual Keyboard" 之类);
|
|
109
|
+
# ≥2 个 = 结果确实在回答查询的不同侧面,不是回声。
|
|
110
|
+
return len(covered_terms) == 1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def query_coverage_ok(results: list[dict[str, Any]], query: str) -> bool:
|
|
114
|
+
"""查询-结果词面覆盖守卫(fast/auto 早停质量门槛)。
|
|
115
|
+
|
|
116
|
+
2026-09-02 实测教训:fast 早停原只看计数+snippet,首引擎上游波动时
|
|
117
|
+
返回 5 条高计数但不相关结果(查询 Crawl4AI 却返回无关 MDN 页),
|
|
118
|
+
早停吞掉 wave-2,单引擎垃圾即成最终答案。本守卫只在极端场景(多数
|
|
119
|
+
结果与查询零词面交集)拒绝早停,让既有 wave-2/串行次引擎补跑——
|
|
120
|
+
只影响「是否停」,不丢弃任何结果,最坏代价是多跑一个引擎。
|
|
121
|
+
|
|
122
|
+
CJK 说明:中文查询分词为单字,覆盖判定在字符级——结果与查询零字符
|
|
123
|
+
交集同样会被拒早停(弱信号但非空);同义词改写场景(如「电脑」vs
|
|
124
|
+
「计算机」)可能多跑一个引擎,属可接受代价:宁可多一次调用,不放走
|
|
125
|
+
单引擎垃圾。
|
|
126
|
+
"""
|
|
127
|
+
if not query or not query.strip():
|
|
128
|
+
return True
|
|
129
|
+
try:
|
|
130
|
+
from tfidf_router import tokenize
|
|
131
|
+
except ImportError:
|
|
132
|
+
return True # 分词不可用不设卡(fail-open)
|
|
133
|
+
q_set = set(tokenize(query))
|
|
134
|
+
if not q_set:
|
|
135
|
+
return True
|
|
136
|
+
# 同构垃圾检测:结构性包索引被域外查询误抢时,会把查询切词后逐词
|
|
137
|
+
# 返回单 token 包名(title≈1 词、URL=/project/<word>),词面覆盖因此
|
|
138
|
+
# 虚高(垃圾包名恰好是查询关键词)→ 覆盖守卫被骗过、垃圾即成最终
|
|
139
|
+
# 答案。多数结果标题 ≤1 token 且查询有 ≥3 token 时,视为索引噪声,
|
|
140
|
+
# 拒绝早停、放行串行次引擎补跑。
|
|
141
|
+
single_token_titles = sum(
|
|
142
|
+
1 for r in results if len(set(tokenize(r.get("title") or ""))) <= 1
|
|
143
|
+
)
|
|
144
|
+
if len(q_set) >= 3 and single_token_titles * 2 > len(results):
|
|
145
|
+
return False
|
|
146
|
+
if is_query_echo(results, query):
|
|
147
|
+
return False
|
|
148
|
+
covered = 0
|
|
149
|
+
for r in results:
|
|
150
|
+
text = f"{r.get('title') or ''} {r.get('snippet') or ''}"
|
|
151
|
+
if q_set & set(tokenize(text)):
|
|
152
|
+
covered += 1
|
|
153
|
+
return covered >= max(1, (len(results) + 1) // 2)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def result_relevance(query: str, results: list[dict[str, Any]],
|
|
157
|
+
top_k: int = 3) -> float | None:
|
|
158
|
+
"""单个引擎这次结果的相关度代理 ∈ [0,1];**拿不到信号时返回 None**。
|
|
159
|
+
|
|
160
|
+
用途:给 `adaptive` 学习器回写质量维度(见 adaptive.quality_factor),
|
|
161
|
+
让引擎排序不再只学「快慢/成败」而能学「好坏」。零网络成本——只读结果
|
|
162
|
+
文本与查询词元。
|
|
163
|
+
|
|
164
|
+
与 `query_coverage_ok` 的区别是本末颠倒:那个问「够不够好到可以停」
|
|
165
|
+
(二元判定,判据保守到宁可多跑一个引擎),这个问「有多好」
|
|
166
|
+
(连续量,用于跨引擎、跨时间比较)。两者共用词面覆盖的直觉,但口径必须
|
|
167
|
+
分开:早停判据要求高敏感度(漏放=放出垃圾),质量分要求可比较
|
|
168
|
+
(量纲稳定,否则引擎之间的分数不可比)。
|
|
169
|
+
|
|
170
|
+
取「前 top_k 条里每条对查询的覆盖率,再取均值」,而不是全量平均:
|
|
171
|
+
一条半相关文档不该替整个结果集背书,反之亦然。
|
|
172
|
+
|
|
173
|
+
返回 None 而非 0.0 是关键:0.0 在自适应评分里是「测过了、很差」,
|
|
174
|
+
None 才是「本次没测」。两者混同会让「无法判定」把引擎打落
|
|
175
|
+
(`adaptive.record` 负责把 None 落成不参与判定的 0.0)。
|
|
176
|
+
"""
|
|
177
|
+
if not query or not query.strip():
|
|
178
|
+
return None
|
|
179
|
+
try:
|
|
180
|
+
from tfidf_router import tokenize
|
|
181
|
+
except ImportError:
|
|
182
|
+
return None # 分词不可用 → 无信号(fail-open,不影响主路径)
|
|
183
|
+
q_set = set(tokenize(query))
|
|
184
|
+
if not q_set:
|
|
185
|
+
return None # 查询无内容词元(纯符号)→ 无法判定
|
|
186
|
+
scored: list[float] = []
|
|
187
|
+
for r in results:
|
|
188
|
+
if not isinstance(r, dict) or "error" in r:
|
|
189
|
+
continue
|
|
190
|
+
text = f"{r.get('title') or ''} {r.get('snippet') or ''}"
|
|
191
|
+
d_set = set(tokenize(text))
|
|
192
|
+
if not d_set:
|
|
193
|
+
scored.append(0.0) # 有结果却零词元:结构化源常见,计为不相关
|
|
194
|
+
continue
|
|
195
|
+
scored.append(len(q_set & d_set) / len(q_set))
|
|
196
|
+
if len(scored) >= top_k:
|
|
197
|
+
break
|
|
198
|
+
if not scored:
|
|
199
|
+
return None # 无有效结果(全部 error / 空列表)→ 无信号
|
|
200
|
+
return sum(scored) / len(scored)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def score_clarity_ok(results: list[dict[str, Any]]) -> bool:
|
|
204
|
+
"""无标注 QPP 信号(2026-09-16):结果分平坦且数量仅达下限时拒绝早停。
|
|
205
|
+
|
|
206
|
+
方法论:无 relevance judgments 的查询性能预测(QPP)——NQC/Clarity 一族
|
|
207
|
+
的共同直觉是「分数分布越尖锐(有明确赢家),查询表现越可预期;分布
|
|
208
|
+
平坦说明检索器对查询拿不准」。这里取最保守的判据:
|
|
209
|
+
|
|
210
|
+
- 只在**计数恰好等于下限**时参与判定(_sufficient_internal 传入
|
|
211
|
+
barely):结果富余本身就是信心,不必再问分数分布;
|
|
212
|
+
- 至少 2 个数值 score 才参与(单条无从谈分布),否则 fail-open
|
|
213
|
+
(放行早停)——下限取 2 而非 3 是刻意的:fast 档计数下限就是 2,
|
|
214
|
+
取 3 会让 fast 档永远够不着信号;两条全等的占位分与三条同样可疑;
|
|
215
|
+
- 全体 score 完全相等(std=0,结构化源常见的占位分)视为最平坦。
|
|
216
|
+
|
|
217
|
+
「拒绝早停」的代价实打实是多跑一个引擎(成本语义见 engine_dispatch
|
|
218
|
+
wave-1 注释),所以阈值取最极端的平坦而非「偏平坦」;宁可多一次调用,
|
|
219
|
+
不放走「字段齐全、计数达标、分数无区分度」的单引擎垃圾(2026-09-02
|
|
220
|
+
实测教训的分数维度补丁,与词面覆盖守卫互补——那个管文本,这个管排序)。
|
|
221
|
+
"""
|
|
222
|
+
scores: list[float] = []
|
|
223
|
+
for r in results:
|
|
224
|
+
try:
|
|
225
|
+
v = r.get("score")
|
|
226
|
+
if v is None:
|
|
227
|
+
continue
|
|
228
|
+
v = float(v)
|
|
229
|
+
except (TypeError, ValueError):
|
|
230
|
+
continue
|
|
231
|
+
scores.append(v)
|
|
232
|
+
if len(scores) < 2:
|
|
233
|
+
return True # 信号不可用:fail-open
|
|
234
|
+
mean = sum(scores) / len(scores)
|
|
235
|
+
if mean <= 0:
|
|
236
|
+
return True
|
|
237
|
+
var = sum((s - mean) ** 2 for s in scores) / len(scores)
|
|
238
|
+
std = var ** 0.5
|
|
239
|
+
return (std / mean) > 0.02 # 变异系数 ≤2% 视为平坦(实测占位分全等=0)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _sufficient_internal(
|
|
243
|
+
results: list[dict[str, Any]],
|
|
244
|
+
mode: str,
|
|
245
|
+
min_results: int | None,
|
|
246
|
+
query: str = "",
|
|
247
|
+
) -> bool:
|
|
248
|
+
"""渐进检索 early-stop:结果是否已够用。
|
|
249
|
+
|
|
250
|
+
轻量启发式(不依赖网络/LLM):
|
|
251
|
+
- 默认 auto:至少 3 条非错误 + 2 条有 snippet
|
|
252
|
+
- 默认 fast:至少 2 条 + 1 个 snippet
|
|
253
|
+
- min_results:域配置覆盖(答案型源 1 条快照即够用,计数语义不动)
|
|
254
|
+
- 通用路径叠加词面覆盖守卫(query_coverage_ok):结果与查询几乎
|
|
255
|
+
无交集时不许早停
|
|
256
|
+
- 两条路径都叠加平坦分守卫(score_clarity_ok):计数恰好压线且
|
|
257
|
+
分数无区分度时不许早停
|
|
258
|
+
|
|
259
|
+
壳链不算「够数」(2026-10-11 热榜实测):early-stop 的两条调用
|
|
260
|
+
路径(wave-1 的 _wave1_stop → results_sufficient、wave-2 的
|
|
261
|
+
cumulative_sufficient)都在此判定,与下游 serp_guard 过滤共用
|
|
262
|
+
同一口径。此前壳链被算作有效结果——wave-1「先合格者赢」时
|
|
263
|
+
baidu_hot 交 10 条 baidu.com/s?wd= 壳即触发早停、同域真榜单
|
|
264
|
+
引擎(toutiao_hot)被 cancelled,壳随后被 serp_guard 整批过滤
|
|
265
|
+
清零、merged 归零触发 2.3s 恢复链。判定与过滤对齐后:壳从一
|
|
266
|
+
开始就不算「够」,早停只在交付真结果的引擎上触发。无 url 的
|
|
267
|
+
结构化快照(行情/天气)不受影响——壳判定只看 URL。
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
from serp_guard import drop_shell_urls as _drop_shells
|
|
271
|
+
except ImportError:
|
|
272
|
+
_drop_shells = None # 守卫不可用 → 壳不过滤(fail-open,与模块容错一致)
|
|
273
|
+
goods: list[dict[str, Any]] = [
|
|
274
|
+
r for r in results if isinstance(r, dict) and "error" not in r]
|
|
275
|
+
if _drop_shells is not None:
|
|
276
|
+
# 壳过滤唯一出口(serp_guard.drop_shell_urls):与下游漏斗过滤、
|
|
277
|
+
# 恢复链 L0/L3 收口共用同一判定,口径不再各自内联漂移。
|
|
278
|
+
goods = _drop_shells(goods)
|
|
279
|
+
if not goods:
|
|
280
|
+
return False
|
|
281
|
+
with_snippet = sum(
|
|
282
|
+
1 for r in goods
|
|
283
|
+
if (r.get("snippet") or r.get("title") or "").strip()
|
|
284
|
+
)
|
|
285
|
+
if min_results is not None:
|
|
286
|
+
try:
|
|
287
|
+
need = max(1, int(min_results))
|
|
288
|
+
except (TypeError, ValueError):
|
|
289
|
+
need = 1
|
|
290
|
+
# 答案型 1 条要求有可展示正文;≥3 条时至少 2 条有 snippet
|
|
291
|
+
need_snip = 1 if need <= 2 else max(2, need - 1)
|
|
292
|
+
ok = len(goods) >= need and with_snippet >= min(need_snip, len(goods))
|
|
293
|
+
# 词面覆盖守卫不适用于答案型(1 条快照无「多数覆盖」可言),
|
|
294
|
+
# 平坦分守卫适用:占位分快照同样可能是垃圾
|
|
295
|
+
return ok and (len(goods) > need or score_clarity_ok(goods))
|
|
296
|
+
threshold = 2 if mode == "fast" else 3
|
|
297
|
+
ok = len(goods) >= threshold and with_snippet >= (1 if mode == "fast" else 2)
|
|
298
|
+
return (ok
|
|
299
|
+
and query_coverage_ok(goods, query)
|
|
300
|
+
and (len(goods) > threshold or score_clarity_ok(goods)))
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""query_templates.py — 查询模板注册表:加载、合并、展开(纯逻辑)。
|
|
3
|
+
|
|
4
|
+
职责单一:把 YAML 注册表读成内存结构,按 id 取模板,把模板 + 追加词展开成
|
|
5
|
+
最终查询串。**不碰 IO 输出面、不 spawn 子进程**——那些在 template_cli.py
|
|
6
|
+
(分工同 citations.py / cite_cli.py)。
|
|
7
|
+
|
|
8
|
+
两个注册表合并(这是「未知使用需求」的普适性所在):
|
|
9
|
+
1. 随包注册表 references/query-templates.yaml(随 argo 升级更新,勿手改)
|
|
10
|
+
2. 用户注册表 $XDG_CONFIG_HOME/argo/query-templates.yaml(缺省 ~/.config/argo/)
|
|
11
|
+
或 ARGO_TEMPLATES_FILE 指定的路径
|
|
12
|
+
用户表按 id 合并:新 id 追加,同名 id 覆盖随包条目;`disabled: true` 停用随包条目。
|
|
13
|
+
于是新增一个领域只要加几行 YAML,零代码、零改仓。
|
|
14
|
+
|
|
15
|
+
为什么注册表是数据而非代码:模板的本质是「命名查询模式」——一个纯数据问题。
|
|
16
|
+
放 YAML 才能让用户零门槛扩展、让 git 管版本、让 --list 自动派生。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
SKILL_DIR = Path(__file__).resolve().parent.parent
|
|
27
|
+
SHIPPED_PATH = SKILL_DIR / "references" / "query-templates.yaml"
|
|
28
|
+
|
|
29
|
+
# command 白名单:模板只能转发到这些 argo 子命令。用户注册表可写,若不设白名单,
|
|
30
|
+
# 一个被篡改/误写的注册表就能借 `argo template` 转发到任意命令——模板是数据,
|
|
31
|
+
# 不该有执行任意命令的能力。
|
|
32
|
+
COMMANDS = ("search", "x-search")
|
|
33
|
+
DEFAULT_COMMAND = "search"
|
|
34
|
+
|
|
35
|
+
_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_]*$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TemplateError(Exception):
|
|
39
|
+
"""注册表缺失/损坏、id 未知、字段非法。"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def user_registry_path() -> Path | None:
|
|
43
|
+
"""用户注册表路径(存在才返回)。优先级:ARGO_TEMPLATES_FILE > 平台配置根 > ~/.config。
|
|
44
|
+
|
|
45
|
+
与 engine_env._envfile_paths 同源思路(复用 _platform_config_root,避免
|
|
46
|
+
「Windows 取哪个目录」这类平台逻辑在本模块再写一份)。
|
|
47
|
+
"""
|
|
48
|
+
explicit = os.environ.get("ARGO_TEMPLATES_FILE", "").strip()
|
|
49
|
+
if explicit:
|
|
50
|
+
return Path(os.path.expanduser(explicit))
|
|
51
|
+
cands: list[Path] = []
|
|
52
|
+
try:
|
|
53
|
+
from engine_env import _platform_config_root
|
|
54
|
+
root = _platform_config_root()
|
|
55
|
+
if root is not None:
|
|
56
|
+
cands.append(root / "argo" / "query-templates.yaml")
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
cands.append(Path(os.path.expanduser("~/.config/argo/query-templates.yaml")))
|
|
60
|
+
for p in cands:
|
|
61
|
+
if p.exists():
|
|
62
|
+
return p
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_yaml(path: Path) -> dict[str, Any]:
|
|
67
|
+
from yaml_load import load as _yaml_load
|
|
68
|
+
try:
|
|
69
|
+
data = _yaml_load(path)
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
raise TemplateError(f"注册表不存在: {path}")
|
|
72
|
+
except Exception as e: # yaml.YAMLError 等
|
|
73
|
+
raise TemplateError(f"注册表解析失败 {path}: {e}")
|
|
74
|
+
if not isinstance(data, dict):
|
|
75
|
+
raise TemplateError(f"注册表顶层必须是映射: {path}")
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _normalize(entry: Any, tid: str, src: str) -> dict[str, Any]:
|
|
80
|
+
"""校验并规范化单条模板。返回 None 之外的 dict。"""
|
|
81
|
+
if not isinstance(entry, dict):
|
|
82
|
+
raise TemplateError(f"{src}: 模板 {tid} 必须是映射")
|
|
83
|
+
if entry.get("disabled"):
|
|
84
|
+
return {"__disabled__": True}
|
|
85
|
+
query = entry.get("query")
|
|
86
|
+
if not isinstance(query, str) or not query.strip():
|
|
87
|
+
raise TemplateError(f"{src}: 模板 {tid} 缺非空 query")
|
|
88
|
+
command = entry.get("command") or DEFAULT_COMMAND
|
|
89
|
+
if command not in COMMANDS:
|
|
90
|
+
raise TemplateError(
|
|
91
|
+
f"{src}: 模板 {tid} 的 command「{command}」不在白名单 {COMMANDS}")
|
|
92
|
+
return {
|
|
93
|
+
"id": tid,
|
|
94
|
+
"description": str(entry.get("description") or "").strip(),
|
|
95
|
+
"category": str(entry.get("category") or "未分组").strip(),
|
|
96
|
+
"command": command,
|
|
97
|
+
"query": query.strip(),
|
|
98
|
+
"tags": [str(t) for t in (entry.get("tags") or []) if str(t).strip()],
|
|
99
|
+
"note": str(entry.get("note") or "").strip(),
|
|
100
|
+
"source": src,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _merge_into(registry: dict[str, dict], data: dict[str, Any], src: str) -> None:
|
|
105
|
+
templates = data.get("templates")
|
|
106
|
+
if templates is None:
|
|
107
|
+
return
|
|
108
|
+
if not isinstance(templates, dict):
|
|
109
|
+
raise TemplateError(f"{src}: templates 必须是映射")
|
|
110
|
+
for tid, entry in templates.items():
|
|
111
|
+
tid = str(tid)
|
|
112
|
+
if not _ID_RE.match(tid):
|
|
113
|
+
raise TemplateError(f"{src}: 模板 id 非法(须 [a-z0-9_]): {tid!r}")
|
|
114
|
+
norm = _normalize(entry, tid, src)
|
|
115
|
+
if norm.get("__disabled__"):
|
|
116
|
+
registry.pop(tid, None) # 用户表可用 disabled 停用随包模板
|
|
117
|
+
continue
|
|
118
|
+
registry[tid] = norm
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_templates(*, include_user: bool = True,
|
|
122
|
+
shipped: Path | None = None,
|
|
123
|
+
user: Path | None = None) -> dict[str, dict]:
|
|
124
|
+
"""加载并合并注册表。返回 {id: 模板};缺失随包表按空处理(不抛)。
|
|
125
|
+
|
|
126
|
+
include_user=False 或显式传 user=None 且不查环境,用于测试与「只看随包」场景。
|
|
127
|
+
"""
|
|
128
|
+
registry: dict[str, dict] = {}
|
|
129
|
+
ship = shipped if shipped is not None else SHIPPED_PATH
|
|
130
|
+
if ship.exists():
|
|
131
|
+
_merge_into(registry, _read_yaml(ship), str(ship))
|
|
132
|
+
if include_user:
|
|
133
|
+
up = user if user is not None else user_registry_path()
|
|
134
|
+
if up is not None and up.exists() and up != ship:
|
|
135
|
+
_merge_into(registry, _read_yaml(up), str(up))
|
|
136
|
+
return registry
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def list_templates(registry: dict[str, dict]) -> list[dict]:
|
|
140
|
+
"""按 (category, id) 排序的模板清单(--list 用)。"""
|
|
141
|
+
return sorted(registry.values(),
|
|
142
|
+
key=lambda t: (t.get("category", ""), t.get("id", "")))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_template(registry: dict[str, dict], tid: str) -> dict:
|
|
146
|
+
"""按 id 取模板;未知 id 抛 TemplateError(带 did-you-mean 建议)。"""
|
|
147
|
+
t = registry.get(tid)
|
|
148
|
+
if t is not None:
|
|
149
|
+
return t
|
|
150
|
+
import difflib
|
|
151
|
+
near = difflib.get_close_matches(tid, list(registry), n=3, cutoff=0.4)
|
|
152
|
+
hint = f"(最接近: {', '.join(near)})" if near else ""
|
|
153
|
+
raise TemplateError(f"未知模板「{tid}」{hint} —— 全部模板见 argo template --list")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def expand(template: dict, adds: list[str] | None = None) -> str:
|
|
157
|
+
"""模板 query + 追加词 → 最终查询串。
|
|
158
|
+
|
|
159
|
+
追加词以空格拼接(X 语法里空格即 AND)。空追加词/空白被丢弃,不产生
|
|
160
|
+
多余空格。这是模板「可组合」的实现:模板给基线,--add 给具体对象。
|
|
161
|
+
"""
|
|
162
|
+
parts = [template.get("query", "")]
|
|
163
|
+
for a in adds or []:
|
|
164
|
+
a = str(a).strip()
|
|
165
|
+
if a:
|
|
166
|
+
parts.append(a)
|
|
167
|
+
return " ".join(p for p in parts if p)
|