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,539 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""engine_env.py — 引擎密钥与启用策略(环境变量优先)
|
|
3
|
+
|
|
4
|
+
规范:
|
|
5
|
+
- 敏感信息只走环境变量,禁止硬编码
|
|
6
|
+
- 新名优先:ARGO_<ENGINE>_API_KEY / ARGO_<NAME>
|
|
7
|
+
- 兼容旧名:TAVILY_API_KEY、EXA_API_KEY、QWEATHER_KEY 等
|
|
8
|
+
- 启用控制:ARGO_ENABLE_ENGINES / ARGO_DISABLE_ENGINES(逗号分隔)
|
|
9
|
+
|
|
10
|
+
对外:
|
|
11
|
+
resolve_env_name / get_env / expand_placeholders
|
|
12
|
+
required_env_for / missing_env_for / env_ready
|
|
13
|
+
parse_engine_list_env / is_engine_allowed_by_env
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import json
|
|
21
|
+
import shlex
|
|
22
|
+
import sys
|
|
23
|
+
import threading
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
# ── 密钥文件热读(平台惯例位置,多候选合并)─────────────────────────────────
|
|
28
|
+
# os.environ 优先(显式覆盖/测试注入),文件保底:进程未经 mcp_launch.sh
|
|
29
|
+
# 注入密钥时仍能拿到,且改文件无需重启——多候选签名缓存,任一变更即重读。
|
|
30
|
+
#
|
|
31
|
+
# 位置按平台惯例解析(候选按优先级排列):
|
|
32
|
+
# 1. ARGO_ENV_FILE:显式指定,**只读这一个**,不做合并(权威覆盖)
|
|
33
|
+
# 2. Windows:%APPDATA%\argo\env;POSIX:$XDG_CONFIG_HOME/argo/env(若该变量设置了)
|
|
34
|
+
# 3. 历史路径 ~/.config/argo/env(POSIX 上它就是 XDG 默认值)
|
|
35
|
+
# 读取时**合并**全部候选(靠前者逐键优先),而不是只认第一个存在的文件:
|
|
36
|
+
# 否则「用户在 XDG 目录放了一份、历史路径还留着一份」会让一部分密钥读得到、
|
|
37
|
+
# 一部分读不到——那正是本仓反复踩过的「装了没通电」形态(引擎按状态层可用、
|
|
38
|
+
# 实际取不到 key,静默 0 结果)。
|
|
39
|
+
_envfile_lock = threading.Lock()
|
|
40
|
+
_envfile_cache: dict[str, str] = {}
|
|
41
|
+
_envfile_sig: tuple = ()
|
|
42
|
+
|
|
43
|
+
ENV_ENV_FILE = "ARGO_ENV_FILE"
|
|
44
|
+
ENV_XDG_CONFIG = "XDG_CONFIG_HOME"
|
|
45
|
+
ENV_APPDATA = "APPDATA"
|
|
46
|
+
_LEGACY_ENVFILE = "~/.config/argo/env"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _platform_config_root(env: "os._Environ[str] | dict[str, str] | None" = None,
|
|
50
|
+
platform: str | None = None) -> Path | None:
|
|
51
|
+
"""平台惯例的配置根(不含应用名):Windows → %APPDATA%;其余 → $XDG_CONFIG_HOME。
|
|
52
|
+
|
|
53
|
+
未设置对应变量时返回 None,由调用方回落到 Unix 惯例 ~/.config。抽成纯函数
|
|
54
|
+
(可注入 env 与 platform)是为了能在 macOS 上直接单测 Windows 分支——否则
|
|
55
|
+
「Windows 上取哪个目录」永远只有真机才能验证。
|
|
56
|
+
"""
|
|
57
|
+
env = os.environ if env is None else env
|
|
58
|
+
platform = sys.platform if platform is None else platform
|
|
59
|
+
if platform.startswith("win"):
|
|
60
|
+
base = str(env.get(ENV_APPDATA) or "").strip()
|
|
61
|
+
else:
|
|
62
|
+
base = str(env.get(ENV_XDG_CONFIG) or "").strip()
|
|
63
|
+
return Path(os.path.expanduser(base)) if base else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _envfile_paths() -> list[Path]:
|
|
67
|
+
"""密钥文件的候选路径,按优先级排列(去重保序)。
|
|
68
|
+
|
|
69
|
+
这里是**唯一**的位置解析入口:读取、签名、诊断都走它,任何新增候选都只
|
|
70
|
+
改这一处(此前写死 ~/.config/argo/env,Windows 与自定义 XDG 都无处安放)。
|
|
71
|
+
"""
|
|
72
|
+
explicit = os.environ.get(ENV_ENV_FILE, "").strip()
|
|
73
|
+
if explicit:
|
|
74
|
+
return [Path(os.path.expanduser(explicit))]
|
|
75
|
+
cands: list[Path] = []
|
|
76
|
+
root = _platform_config_root()
|
|
77
|
+
if root is not None:
|
|
78
|
+
cands.append(root / "argo" / "env")
|
|
79
|
+
cands.append(Path(os.path.expanduser(_LEGACY_ENVFILE)))
|
|
80
|
+
out: list[Path] = []
|
|
81
|
+
for p in cands:
|
|
82
|
+
if p not in out:
|
|
83
|
+
out.append(p)
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _envfile_path() -> Path:
|
|
88
|
+
"""首选(最高优先级)密钥文件路径——展示与错误提示用。
|
|
89
|
+
|
|
90
|
+
读取请用 `_envfile_paths()`:只认这一个会漏掉另一份候选里的密钥。
|
|
91
|
+
"""
|
|
92
|
+
return _envfile_paths()[0]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _parse_envfile(text: str) -> dict[str, str]:
|
|
96
|
+
"""解析 env 文件文本(export 前缀、引号、注释都按既有计算方式处理)。"""
|
|
97
|
+
data: dict[str, str] = {}
|
|
98
|
+
for line in text.splitlines():
|
|
99
|
+
line = line.strip()
|
|
100
|
+
if not line or line.startswith("#"):
|
|
101
|
+
continue
|
|
102
|
+
if line.startswith("export "):
|
|
103
|
+
line = line[len("export "):]
|
|
104
|
+
if "=" not in line:
|
|
105
|
+
continue
|
|
106
|
+
k, _, v = line.partition("=")
|
|
107
|
+
k = k.strip()
|
|
108
|
+
v = v.strip()
|
|
109
|
+
if not k or not v:
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
parts = shlex.split(v)
|
|
113
|
+
val = parts[0] if parts else ""
|
|
114
|
+
except ValueError:
|
|
115
|
+
val = v.strip("'\"")
|
|
116
|
+
data[k] = val
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _envfile_load() -> dict[str, str]:
|
|
121
|
+
global _envfile_cache, _envfile_sig
|
|
122
|
+
paths = _envfile_paths()
|
|
123
|
+
stats: list[tuple[str, int, int]] = []
|
|
124
|
+
for p in paths:
|
|
125
|
+
try:
|
|
126
|
+
st = p.stat()
|
|
127
|
+
stats.append((str(p), st.st_mtime_ns, st.st_size))
|
|
128
|
+
except OSError:
|
|
129
|
+
continue
|
|
130
|
+
sig = tuple(stats)
|
|
131
|
+
with _envfile_lock:
|
|
132
|
+
if sig == _envfile_sig:
|
|
133
|
+
return _envfile_cache
|
|
134
|
+
data: dict[str, str] = {}
|
|
135
|
+
# 靠前的候选优先:先读的键不被后面的覆盖(显式覆盖 > 平台惯例 > 历史)
|
|
136
|
+
for p in paths:
|
|
137
|
+
try:
|
|
138
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
139
|
+
except OSError:
|
|
140
|
+
continue
|
|
141
|
+
for k, v in _parse_envfile(text).items():
|
|
142
|
+
data.setdefault(k, v)
|
|
143
|
+
_envfile_cache = data
|
|
144
|
+
_envfile_sig = sig
|
|
145
|
+
return data
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def reset_envfile_cache() -> None:
|
|
149
|
+
"""清空 env 文件缓存,强制下次读取重新读盘。
|
|
150
|
+
|
|
151
|
+
缓存内容与签名是一对状态,必须一起清。只把签名改回旧值会让本进程内的
|
|
152
|
+
密钥读取**永久失效**:签名与真实文件对得上,函数就直接返回那份不属于
|
|
153
|
+
该文件的缓存。2026-09-15 实测到的现场——某测试用「改签名 + 临时路径」
|
|
154
|
+
强制重读,结束时只恢复签名,于是此后 get_env 再也读不到
|
|
155
|
+
~/.config/argo/env 里的任何密钥,路由把带密钥的引擎全判为不可用,
|
|
156
|
+
表现为「单独跑通过、和别的测试一起跑就失败」。
|
|
157
|
+
|
|
158
|
+
需要强制重读的调用方(测试隔离、密钥热更新)都走这里,别再各自操作
|
|
159
|
+
那两个全局变量:只做一半的状态在测试里几乎看不出来,排查成本极高。
|
|
160
|
+
"""
|
|
161
|
+
global _envfile_cache, _envfile_sig
|
|
162
|
+
with _envfile_lock:
|
|
163
|
+
_envfile_cache = {}
|
|
164
|
+
_envfile_sig = ()
|
|
165
|
+
|
|
166
|
+
# 引擎 → 候选环境变量(从左到右优先)
|
|
167
|
+
# 第一项为推荐新名(ARGO_ 前缀),后续为历史兼容名
|
|
168
|
+
KNOWN_ENV_ALIASES: dict[str, list[str]] = {
|
|
169
|
+
"tavily": ["ARGO_TAVILY_API_KEY", "TAVILY_API_KEY"],
|
|
170
|
+
"bocha": ["ARGO_BOCHA_API_KEY", "BOCHA_API_KEY"],
|
|
171
|
+
"bocha_ai": ["ARGO_BOCHA_API_KEY", "BOCHA_API_KEY"],
|
|
172
|
+
"brave": ["ARGO_BRAVE_API_KEY", "BRAVE_API_KEY"],
|
|
173
|
+
"byted": ["ARGO_BYTED_API_KEY", "ARGO_WEB_SEARCH_API_KEY", "WEB_SEARCH_API_KEY"],
|
|
174
|
+
"exa": ["ARGO_EXA_API_KEY", "EXA_API_KEY"],
|
|
175
|
+
"octen": ["ARGO_OCTEN_API_KEY", "OCTEN_API_KEY"],
|
|
176
|
+
"felo": ["ARGO_FELO_API_KEY", "FELO_API_KEY"],
|
|
177
|
+
"metaso": ["ARGO_METASO_API_KEY", "METASO_API_KEY"],
|
|
178
|
+
"qweather": ["ARGO_QWEATHER_KEY", "QWEATHER_KEY"],
|
|
179
|
+
"github": ["ARGO_GITHUB_TOKEN", "GITHUB_TOKEN"],
|
|
180
|
+
"wolframalpha": ["ARGO_WOLFRAM_APPID", "WOLFRAM_APPID"],
|
|
181
|
+
"zhihu": ["ARGO_ZHIHU_ACCESS_SECRET", "ZHIHU_ACCESS_SECRET"],
|
|
182
|
+
"zhihu_global": ["ARGO_ZHIHU_ACCESS_SECRET", "ZHIHU_ACCESS_SECRET"],
|
|
183
|
+
"zhihu_hot": ["ARGO_ZHIHU_ACCESS_SECRET", "ZHIHU_ACCESS_SECRET"],
|
|
184
|
+
"zhihu_user": ["ARGO_ZHIHU_ACCESS_SECRET", "ZHIHU_ACCESS_SECRET"],
|
|
185
|
+
"anysearch": ["ARGO_ANYSEARCH_API_KEY", "ANYSEARCH_API_KEY"], # 可选
|
|
186
|
+
"firecrawl": ["ARGO_FIRECRAWL_API_KEY", "FIRECRAWL_API_KEY"], # 可选(keyless 免费层)
|
|
187
|
+
"weread": ["ARGO_WEREAD_API_KEY", "WEREAD_API_KEY"], # 微信读书 Agent Gateway
|
|
188
|
+
"em_miaoxiang": ["ARGO_EASTMONEY_APIKEY", "EASTMONEY_APIKEY"], # 东财妙想(可选)
|
|
189
|
+
# 腾讯元宝搜索:官方文档用的名字是 TENCENTCLOUD_WSA_APIKEY,
|
|
190
|
+
# argo 推荐名在前;两个都认,便于照文档配的用户直接可用。
|
|
191
|
+
"yuanbao": ["ARGO_YUANBAO_API_KEY", "TENCENTCLOUD_WSA_APIKEY",
|
|
192
|
+
"YUANBAO_API_KEY"],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# 占位符名 → 候选 env(用于 config 中 {TAVILY_API_KEY} 展开)
|
|
196
|
+
PLACEHOLDER_ALIASES: dict[str, list[str]] = {
|
|
197
|
+
"TAVILY_API_KEY": ["ARGO_TAVILY_API_KEY", "TAVILY_API_KEY"],
|
|
198
|
+
"BOCHA_API_KEY": ["ARGO_BOCHA_API_KEY", "BOCHA_API_KEY"],
|
|
199
|
+
"BRAVE_API_KEY": ["ARGO_BRAVE_API_KEY", "BRAVE_API_KEY"],
|
|
200
|
+
"WEB_SEARCH_API_KEY": ["ARGO_BYTED_API_KEY", "ARGO_WEB_SEARCH_API_KEY", "WEB_SEARCH_API_KEY"],
|
|
201
|
+
"EXA_API_KEY": ["ARGO_EXA_API_KEY", "EXA_API_KEY"],
|
|
202
|
+
"OCTEN_API_KEY": ["ARGO_OCTEN_API_KEY", "OCTEN_API_KEY"],
|
|
203
|
+
"FELO_API_KEY": ["ARGO_FELO_API_KEY", "FELO_API_KEY"],
|
|
204
|
+
"METASO_API_KEY": ["ARGO_METASO_API_KEY", "METASO_API_KEY"],
|
|
205
|
+
"QWEATHER_KEY": ["ARGO_QWEATHER_KEY", "QWEATHER_KEY"],
|
|
206
|
+
"GITHUB_TOKEN": ["ARGO_GITHUB_TOKEN", "GITHUB_TOKEN"],
|
|
207
|
+
"WOLFRAM_APPID": ["ARGO_WOLFRAM_APPID", "WOLFRAM_APPID"],
|
|
208
|
+
"ZHIHU_ACCESS_SECRET": ["ARGO_ZHIHU_ACCESS_SECRET", "ZHIHU_ACCESS_SECRET"],
|
|
209
|
+
"ANYSEARCH_API_KEY": ["ARGO_ANYSEARCH_API_KEY", "ANYSEARCH_API_KEY"],
|
|
210
|
+
"FIRECRAWL_API_KEY": ["ARGO_FIRECRAWL_API_KEY", "FIRECRAWL_API_KEY"],
|
|
211
|
+
"WEREAD_API_KEY": ["ARGO_WEREAD_API_KEY", "WEREAD_API_KEY"],
|
|
212
|
+
"UNPAYWALL_EMAIL": ["ARGO_UNPAYWALL_EMAIL", "UNPAYWALL_EMAIL"],
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_PLACEHOLDER_RE = re.compile(r"\{([A-Z_][A-Z0-9_]*)\}")
|
|
216
|
+
# 动态生成的非密钥占位符,不算 required_env
|
|
217
|
+
# (UUID = engines_base._proc_uuid 的进程级一次性身份,见其 docstring)
|
|
218
|
+
_NON_SECRET_PLACEHOLDERS = {"QUERY", "N", "TIMESTAMP", "MODE", "DEPTH", "UUID"}
|
|
219
|
+
|
|
220
|
+
# 可选密钥:有则更好,缺失不阻断自动路由
|
|
221
|
+
OPTIONAL_ENV_ENGINES: set[str] = {
|
|
222
|
+
"exa", # 无 key 走官方 MCP 匿名通道(engines_builders_tech._exa_mcp_keyless)
|
|
223
|
+
"github", # GITHUB_TOKEN 仅提高限频
|
|
224
|
+
"anysearch", # ANYSEARCH_API_KEY 可选
|
|
225
|
+
"firecrawl", # FIRECRAWL_API_KEY 可选:官方免费层匿名(keyless)即可搜索
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _name_variants(name: str) -> list[str]:
|
|
230
|
+
"""单个名字展开为候选链:原名 + 前缀规范名(去重保序)。
|
|
231
|
+
|
|
232
|
+
密钥有两套写法:推荐名 `ARGO_<NAME>` 与历史裸名 `<NAME>`,用户只写其中
|
|
233
|
+
一套是常态(文档推 ARGO_ 名、老集成留裸名)。所以「单名」入口必须两套
|
|
234
|
+
都认——否则同一把 key 会出现「一部分读得到、一部分读不到」的分裂:
|
|
235
|
+
`get_env("TINYFISH_API_KEY")` 只查裸名,而 config.yaml / ENGINE_CATALOG
|
|
236
|
+
都要求配 `ARGO_TINYFISH_API_KEY`,实测结果是 fetch 渲染层被悄悄关掉
|
|
237
|
+
(`_api_key()` 拿到空串)而 search 三引擎正常。
|
|
238
|
+
"""
|
|
239
|
+
if not name:
|
|
240
|
+
return []
|
|
241
|
+
if name.startswith("ARGO_"):
|
|
242
|
+
return [name, name[len("ARGO_"):]]
|
|
243
|
+
return [name, f"ARGO_{name}"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_env(names: str | list[str], default: str = "") -> str:
|
|
247
|
+
"""按优先级读取第一个非空环境变量;os.environ 优先,~/.config/argo/env
|
|
248
|
+
热读保底(改文件即生效,无需重启)。
|
|
249
|
+
|
|
250
|
+
单名字符串会展开为「原名 + 前缀变体」候选链(见 _name_variants);显式
|
|
251
|
+
列表按调用方给定顺序与内容原样使用(调用方已写全两个名字时不做二次展开,
|
|
252
|
+
保持既有优先级语义)。"""
|
|
253
|
+
if isinstance(names, str):
|
|
254
|
+
names = _name_variants(names)
|
|
255
|
+
for name in names:
|
|
256
|
+
if not name:
|
|
257
|
+
continue
|
|
258
|
+
val = os.environ.get(name)
|
|
259
|
+
if val is not None and str(val).strip() != "":
|
|
260
|
+
return str(val)
|
|
261
|
+
try:
|
|
262
|
+
file_env = _envfile_load()
|
|
263
|
+
except Exception:
|
|
264
|
+
file_env = {}
|
|
265
|
+
for name in names:
|
|
266
|
+
if not name:
|
|
267
|
+
continue
|
|
268
|
+
val = file_env.get(name)
|
|
269
|
+
if val is not None and str(val).strip() != "":
|
|
270
|
+
return str(val)
|
|
271
|
+
return default
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# 布尔开关的「关」值集合(统一定义在这一处,见 env_flag)
|
|
275
|
+
_FALSY_VALUES = frozenset({
|
|
276
|
+
"0", "false", "no", "off", "n", "disable", "disabled", "none",
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
# 授权位的「开」值白名单(strict=True 时使用):只有明确写出的真值算放行
|
|
280
|
+
_TRUE_VALUES = frozenset({"1", "true", "yes", "on", "y", "t"})
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def env_flag(name: str, default: bool = True, *, expand: bool = True,
|
|
284
|
+
strict: bool = False) -> bool:
|
|
285
|
+
"""布尔环境开关的统一解析(全仓只有这一处)。
|
|
286
|
+
|
|
287
|
+
此前全仓有四套互不兼容的判断规则,同一写法在不同开关上行为不同:
|
|
288
|
+
- `not in ("0","false","False")` → 不认 no/off(ARGO_FETCH_PARALLEL、
|
|
289
|
+
ARGO_USAGE_LOG)
|
|
290
|
+
- `not in ("0","false","False","no")` → 不认 off(fetch_v3 的四个开关、
|
|
291
|
+
引擎 HTTP、robots、tinyfish、search 两处)
|
|
292
|
+
- `not in {"","0","false","off","no"}` → 空串算关(recompute,语义不同)
|
|
293
|
+
用户写 `ARGO_FETCH_JINA=off` 关得掉,写 `ARGO_FETCH_PARALLEL=off` 却关不掉
|
|
294
|
+
——「开关看着生效、其实没生效」是最难查的一类问题。
|
|
295
|
+
|
|
296
|
+
判定规则:忽略大小写与首尾空白,以下算关:0/false/no/off/n/disable/disabled/none。
|
|
297
|
+
未设置或值为空 → default。读取走 get_env:os.environ 优先,随后
|
|
298
|
+
~/.config/argo/env 热读——此前这些开关只认 os.environ,把开关写进 env
|
|
299
|
+
文件(密钥的规范位置)是不生效的。
|
|
300
|
+
|
|
301
|
+
expand=False 只认**字面名字**,不做别名展开。授权类开关(决定「是否放行
|
|
302
|
+
一个受限动作」的开关,如 ARGO_ALLOW_RECOMPUTE)必须用它:别名展开是给
|
|
303
|
+
密钥用的便利(`X_API_KEY` 与 `ARGO_X_API_KEY` 都认),一旦落在授权位上就
|
|
304
|
+
变成了授权扩张——环境里任何一个工具随手设 `ALLOW_RECOMPUTE=1`(少写了
|
|
305
|
+
ARGO_ 前缀的一个无关变量)就等于替用户放行了受限子进程执行脚本。授权只
|
|
306
|
+
认推荐名这一条明确信号,误触不了;能力开关(超时、并发、渲染降级)继续
|
|
307
|
+
保留两套写法的容忍度。
|
|
308
|
+
|
|
309
|
+
strict=True 只认**明确写出的真值**(1/true/yes/on/y/t),其余一律算关。
|
|
310
|
+
授权位必须同时用 expand=False + strict=True:默认计算方式是「非关即开」,那对
|
|
311
|
+
能力开关没问题(最坏是换个行为),对授权位就是「任何拼错的值都放行」——
|
|
312
|
+
`ARGO_ALLOW_RECOMPUTE=0x0`、`=maybe`、`=ture` 全都会授权。检查见
|
|
313
|
+
tests/test_static_lint_gate.py(漏写任一项即报红)。
|
|
314
|
+
"""
|
|
315
|
+
raw = get_env(_name_variants(name) if expand else [name])
|
|
316
|
+
val = str(raw).strip().lower()
|
|
317
|
+
if val == "":
|
|
318
|
+
return default
|
|
319
|
+
if strict:
|
|
320
|
+
return val in _TRUE_VALUES
|
|
321
|
+
return val not in _FALSY_VALUES
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def sync_envfile_to_environ() -> list[str]:
|
|
325
|
+
"""把 ~/.config/argo/env 同步进 os.environ(只填缺失,不覆盖已有值)。
|
|
326
|
+
|
|
327
|
+
兼容计算方式:读取方保持标准 os.environ 直读不动(含其他 AGT/客户端的
|
|
328
|
+
既有集成,零改动零破坏),由入口(bin/argo / mcp_server)调用本函数
|
|
329
|
+
把文件密钥「同步一份过去」。os.environ 已有变量永远优先——显式覆盖
|
|
330
|
+
与测试注入不受影响。重复执行结果一致,可重复调用。
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
file_env = _envfile_load()
|
|
334
|
+
except Exception:
|
|
335
|
+
return []
|
|
336
|
+
injected: list[str] = []
|
|
337
|
+
for k, v in file_env.items():
|
|
338
|
+
cur = os.environ.get(k)
|
|
339
|
+
if cur is None or cur.strip() == "":
|
|
340
|
+
os.environ[k] = v
|
|
341
|
+
injected.append(k)
|
|
342
|
+
return injected
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def resolve_env_name(engine_id: str, logical: str = "api_key") -> list[str]:
|
|
346
|
+
"""返回某引擎逻辑密钥的候选环境变量名列表。"""
|
|
347
|
+
aliases = KNOWN_ENV_ALIASES.get(engine_id)
|
|
348
|
+
if aliases:
|
|
349
|
+
return list(aliases)
|
|
350
|
+
upper = engine_id.upper().replace("-", "_")
|
|
351
|
+
if logical == "api_key":
|
|
352
|
+
return [f"ARGO_{upper}_API_KEY", f"{upper}_API_KEY"]
|
|
353
|
+
upper_logic = logical.upper()
|
|
354
|
+
return [f"ARGO_{upper}_{upper_logic}", f"{upper}_{upper_logic}"]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def expand_placeholders(value: Any) -> Any:
|
|
358
|
+
"""递归展开字符串中的 {ENV_NAME},支持 ARGO_ 别名。"""
|
|
359
|
+
if isinstance(value, str):
|
|
360
|
+
def _sub(m: re.Match[str]) -> str:
|
|
361
|
+
key = m.group(1)
|
|
362
|
+
if key in _NON_SECRET_PLACEHOLDERS:
|
|
363
|
+
return m.group(0)
|
|
364
|
+
candidates = PLACEHOLDER_ALIASES.get(key, [f"ARGO_{key}", key])
|
|
365
|
+
resolved = get_env(candidates, "")
|
|
366
|
+
return resolved if resolved else m.group(0)
|
|
367
|
+
|
|
368
|
+
return _PLACEHOLDER_RE.sub(_sub, value)
|
|
369
|
+
if isinstance(value, list):
|
|
370
|
+
return [expand_placeholders(v) for v in value]
|
|
371
|
+
if isinstance(value, dict):
|
|
372
|
+
return {k: expand_placeholders(v) for k, v in value.items()}
|
|
373
|
+
return value
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _placeholders_in_spec(spec: dict[str, Any]) -> list[str]:
|
|
377
|
+
"""从引擎 spec 中提取 {ENV} 占位符名(不含非密钥)。"""
|
|
378
|
+
try:
|
|
379
|
+
blob = json.dumps(spec, ensure_ascii=False, default=str)
|
|
380
|
+
except Exception:
|
|
381
|
+
blob = str(spec)
|
|
382
|
+
found = []
|
|
383
|
+
for m in _PLACEHOLDER_RE.finditer(blob):
|
|
384
|
+
key = m.group(1)
|
|
385
|
+
if key in _NON_SECRET_PLACEHOLDERS:
|
|
386
|
+
continue
|
|
387
|
+
if key not in found:
|
|
388
|
+
found.append(key)
|
|
389
|
+
return found
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def required_env_for(engine_id: str, spec: dict[str, Any] | None = None) -> list[str]:
|
|
393
|
+
"""推断引擎必填环境变量(返回「逻辑/主推荐」名列表,用于展示与检查)。
|
|
394
|
+
|
|
395
|
+
优先级:
|
|
396
|
+
1. spec.required_env(显式列表,元素可以是 str 或候选列表)
|
|
397
|
+
2. KNOWN_ENV_ALIASES
|
|
398
|
+
3. 从 spec 占位符推断
|
|
399
|
+
4. 专用 type 映射
|
|
400
|
+
"""
|
|
401
|
+
spec = spec or {}
|
|
402
|
+
if engine_id in OPTIONAL_ENV_ENGINES and not spec.get("require_api_key"):
|
|
403
|
+
# 仍可在 detail 中展示 optional,但不作为 required
|
|
404
|
+
return []
|
|
405
|
+
|
|
406
|
+
explicit = spec.get("required_env")
|
|
407
|
+
if explicit:
|
|
408
|
+
out: list[str] = []
|
|
409
|
+
for item in explicit:
|
|
410
|
+
if isinstance(item, list) and item:
|
|
411
|
+
out.append(item[0])
|
|
412
|
+
elif isinstance(item, str) and item:
|
|
413
|
+
out.append(item)
|
|
414
|
+
return out
|
|
415
|
+
|
|
416
|
+
if engine_id in KNOWN_ENV_ALIASES:
|
|
417
|
+
return [KNOWN_ENV_ALIASES[engine_id][0]]
|
|
418
|
+
|
|
419
|
+
# type 级默认
|
|
420
|
+
type_defaults = {
|
|
421
|
+
"exa": ["ARGO_EXA_API_KEY"],
|
|
422
|
+
"octen": ["ARGO_OCTEN_API_KEY"],
|
|
423
|
+
"qweather": ["ARGO_QWEATHER_KEY"],
|
|
424
|
+
}
|
|
425
|
+
t = str(spec.get("type", ""))
|
|
426
|
+
if t in type_defaults:
|
|
427
|
+
return type_defaults[t]
|
|
428
|
+
|
|
429
|
+
placeholders = _placeholders_in_spec(spec)
|
|
430
|
+
# 把占位符映射到推荐 ARGO 名
|
|
431
|
+
mapped: list[str] = []
|
|
432
|
+
for p in placeholders:
|
|
433
|
+
candidates = PLACEHOLDER_ALIASES.get(p, [f"ARGO_{p}", p])
|
|
434
|
+
mapped.append(candidates[0])
|
|
435
|
+
return mapped
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def missing_env_for(engine_id: str, spec: dict[str, Any] | None = None) -> list[str]:
|
|
439
|
+
"""返回缺失的环境变量(用候选链检查:任一候选有值即视为满足)。"""
|
|
440
|
+
spec = spec or {}
|
|
441
|
+
if engine_id in OPTIONAL_ENV_ENGINES and not spec.get("require_api_key"):
|
|
442
|
+
return []
|
|
443
|
+
|
|
444
|
+
missing: list[str] = []
|
|
445
|
+
|
|
446
|
+
explicit = spec.get("required_env")
|
|
447
|
+
if explicit:
|
|
448
|
+
for item in explicit:
|
|
449
|
+
if isinstance(item, list):
|
|
450
|
+
candidates = item
|
|
451
|
+
display = item[0] if item else "?"
|
|
452
|
+
else:
|
|
453
|
+
display = str(item)
|
|
454
|
+
if display in PLACEHOLDER_ALIASES:
|
|
455
|
+
candidates = PLACEHOLDER_ALIASES[display]
|
|
456
|
+
elif display.startswith("ARGO_"):
|
|
457
|
+
legacy = display[len("ARGO_"):]
|
|
458
|
+
candidates = [display, legacy]
|
|
459
|
+
else:
|
|
460
|
+
candidates = [f"ARGO_{display}", display]
|
|
461
|
+
if not get_env(candidates):
|
|
462
|
+
missing.append(display)
|
|
463
|
+
return missing
|
|
464
|
+
|
|
465
|
+
if engine_id in KNOWN_ENV_ALIASES:
|
|
466
|
+
aliases = KNOWN_ENV_ALIASES[engine_id]
|
|
467
|
+
if not get_env(aliases):
|
|
468
|
+
missing.append(aliases[0])
|
|
469
|
+
return missing
|
|
470
|
+
|
|
471
|
+
t = str((spec or {}).get("type", ""))
|
|
472
|
+
type_map = {
|
|
473
|
+
"exa": KNOWN_ENV_ALIASES["exa"],
|
|
474
|
+
"octen": KNOWN_ENV_ALIASES["octen"],
|
|
475
|
+
"qweather": KNOWN_ENV_ALIASES["qweather"],
|
|
476
|
+
}
|
|
477
|
+
if t in type_map:
|
|
478
|
+
if not get_env(type_map[t]):
|
|
479
|
+
missing.append(type_map[t][0])
|
|
480
|
+
return missing
|
|
481
|
+
|
|
482
|
+
for p in _placeholders_in_spec(spec or {}):
|
|
483
|
+
candidates = PLACEHOLDER_ALIASES.get(p, [f"ARGO_{p}", p])
|
|
484
|
+
if not get_env(candidates):
|
|
485
|
+
missing.append(candidates[0])
|
|
486
|
+
return missing
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def env_ready(engine_id: str, spec: dict[str, Any] | None = None) -> bool:
|
|
490
|
+
return len(missing_env_for(engine_id, spec)) == 0
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def parse_engine_list_env(var_name: str) -> set[str] | None:
|
|
494
|
+
"""解析逗号分隔引擎列表;未设置返回 None。"""
|
|
495
|
+
raw = os.environ.get(var_name, "").strip()
|
|
496
|
+
if not raw:
|
|
497
|
+
return None
|
|
498
|
+
return {p.strip() for p in raw.split(",") if p.strip()}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def is_engine_allowed_by_env(engine_id: str) -> bool:
|
|
502
|
+
"""ARGO_ENABLE_ENGINES 白名单 / ARGO_DISABLE_ENGINES 黑名单。
|
|
503
|
+
|
|
504
|
+
- 仅设置 ENABLE:不在名单内 → False
|
|
505
|
+
- DISABLE 命中 → False
|
|
506
|
+
- 都未设置 → True
|
|
507
|
+
"""
|
|
508
|
+
enabled = parse_engine_list_env("ARGO_ENABLE_ENGINES")
|
|
509
|
+
disabled = parse_engine_list_env("ARGO_DISABLE_ENGINES")
|
|
510
|
+
if disabled and engine_id in disabled:
|
|
511
|
+
return False
|
|
512
|
+
if enabled is not None and engine_id not in enabled:
|
|
513
|
+
return False
|
|
514
|
+
return True
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def env_status_for(engine_id: str, spec: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
518
|
+
"""单引擎 env 状态摘要(供 list/validate)。"""
|
|
519
|
+
required = required_env_for(engine_id, spec)
|
|
520
|
+
missing = missing_env_for(engine_id, spec)
|
|
521
|
+
return {
|
|
522
|
+
"required_env": required,
|
|
523
|
+
"missing_env": missing,
|
|
524
|
+
"env_ready": len(missing) == 0,
|
|
525
|
+
"allowed_by_env": is_engine_allowed_by_env(engine_id),
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# ── CLI 入口的 env 文件自动同步 ──────────────────────────────────────────────
|
|
530
|
+
# MCP server 在入口显式调 sync_envfile_to_environ();CLI 的 scripts/*.py 此前
|
|
531
|
+
# 没有任何同步点,导致约 22 处 os.environ 直读的 ARGO_* 开关在 CLI 下不认
|
|
532
|
+
# env 文件(net_proxy.py 头注记录的「MCP 同步、CLI 不同步」分裂)。放在模块
|
|
533
|
+
# 加载时一次性幂等同步:import 本模块的 CLI 入口即被覆盖,子进程经 environ
|
|
534
|
+
# 继承同样受益。os.environ 已有值永远优先——显式覆盖与测试注入不受影响;
|
|
535
|
+
# 与 get_env 的热读语义同源,只是把同一份数据提前灌进 environ。
|
|
536
|
+
try:
|
|
537
|
+
sync_envfile_to_environ()
|
|
538
|
+
except Exception:
|
|
539
|
+
pass # env 文件缺失/损坏时静默走 os.environ 现状,不阻塞 import
|