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,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lang_matrix_probe.py — 多语言 × 引擎 能力矩阵实测(C 阶段量化基线)。
|
|
3
|
+
|
|
4
|
+
## 为什么需要它
|
|
5
|
+
|
|
6
|
+
argo 的对外宣称是「多语言检测与跨语言回退」,但实测发现两个未见底的问题:
|
|
7
|
+
1. 语言检测只有「语系级」精度:日语「人工知能」被判成 zh
|
|
8
|
+
2. 引擎在多语言下的**噪声率未知**:juejin 在阿/俄/德查询下返回 10 条、
|
|
9
|
+
相关度 0.00(纯噪声),但引擎报告 status=ok
|
|
10
|
+
|
|
11
|
+
没有完整矩阵,任何「该补哪门语言」的决策都是拍脑袋。本脚本产出
|
|
12
|
+
「语言 × 引擎」的返回量 + 相关度,作为 B 阶段的决策依据。
|
|
13
|
+
|
|
14
|
+
## 度量计算方式
|
|
15
|
+
|
|
16
|
+
对每个 (引擎, 语言) 组合:
|
|
17
|
+
- count :返回条数(0 = 无能力)
|
|
18
|
+
- rel :相关度均值 0~1(查询词元出现在标题/摘要的比例)
|
|
19
|
+
- verdict :相关(>=0.5) / 部分(>=0.2) / 噪声(<0.2) / 空
|
|
20
|
+
|
|
21
|
+
相关度用「查询词元命中率」而非语义相似度:廉价、可解释、无需模型,
|
|
22
|
+
足以区分「真结果」与「热帖噪声」——实测 juejin 噪声案例得 0.00,
|
|
23
|
+
真结果得 0.5~1.0,判别力充分。
|
|
24
|
+
|
|
25
|
+
用法:
|
|
26
|
+
python3 scripts/lang_matrix_probe.py --json /tmp/matrix.json
|
|
27
|
+
python3 scripts/lang_matrix_probe.py --langs zh,ja,de --engines wikipedia,qiita
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
# 状态隔离(2026-10-10 方案甲):走真实搜索路径的脚本会写生产状态(实测夹具
|
|
33
|
+
# 引擎已混入 circuit_breaker/usage_log)。须先于 argo 状态模块导入重定向
|
|
34
|
+
# (机制见 argo_paths.isolate_state_dir docstring);被测试导入时不生效。
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
from argo_paths import isolate_state_dir
|
|
37
|
+
isolate_state_dir("argo-bench")
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import json
|
|
41
|
+
import logging
|
|
42
|
+
import re
|
|
43
|
+
import sys
|
|
44
|
+
import time
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from cli_io import dumps_pretty
|
|
47
|
+
|
|
48
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
49
|
+
if str(SCRIPT_DIR) not in sys.path:
|
|
50
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
51
|
+
|
|
52
|
+
logging.disable(logging.CRITICAL)
|
|
53
|
+
|
|
54
|
+
# ── 必须最先执行:把 ~/.config/argo/env 的密钥同步进 os.environ ───────────
|
|
55
|
+
# 实测教训:首版探针直接 `import engines` 调用,绕过了 bin/argo 入口的
|
|
56
|
+
# sync_envfile_to_environ(),导致所有靠文件密钥的引擎(bocha/exa/octen/
|
|
57
|
+
# anysearch…)全部误报失败——因为 builder 用 os.environ 直读,
|
|
58
|
+
# 而密钥只在文件里。修法不是改探针调用方式,而是显式补上同步这一步:
|
|
59
|
+
# 凡是「绕过 CLI 入口直接调库」的场景都必须自己做这件事。
|
|
60
|
+
try:
|
|
61
|
+
from engine_env import sync_envfile_to_environ
|
|
62
|
+
_SYNCED = sync_envfile_to_environ()
|
|
63
|
+
except Exception:
|
|
64
|
+
_SYNCED = []
|
|
65
|
+
|
|
66
|
+
# ── 语料:每种语言一个「语义等价」的查询(同一个主题:AI 进展)─────────────
|
|
67
|
+
# 语义等价是关键:这样不同语言的横向对比才有意义(比较的是「引擎能力」
|
|
68
|
+
# 而不是「主题差异」)。部分语言另配一个本地化主题以覆盖文化类引擎。
|
|
69
|
+
QUERIES: dict[str, list[str]] = {
|
|
70
|
+
"zh": ["人工智能 最新进展", "北京 旅游 攻略"],
|
|
71
|
+
"en": ["artificial intelligence progress", "new york travel guide"],
|
|
72
|
+
"ja": ["人工知能 最新 動向", "東京 観光 おすすめ"],
|
|
73
|
+
"ko": ["인공지능 최신 동향", "서울 여행 추천"],
|
|
74
|
+
"ru": ["искусственный интеллект", "москва путешествие"],
|
|
75
|
+
"ar": ["الذكاء الاصطناعي", "دبي سياحة"],
|
|
76
|
+
"de": ["künstliche Intelligenz", "berlin reise"],
|
|
77
|
+
"fr": ["intelligence artificielle", "paris voyage"],
|
|
78
|
+
"es": ["inteligencia artificial", "madrid viaje"],
|
|
79
|
+
"pt": ["inteligência artificial", "lisboa viagem"],
|
|
80
|
+
"it": ["intelligenza artificiale", "roma viaggio"],
|
|
81
|
+
"nl": ["kunstmatige intelligentie", "amsterdam reis"],
|
|
82
|
+
"pl": ["sztuczna inteligencja", "warszawa podróż"],
|
|
83
|
+
"tr": ["yapay zeka", "istanbul seyahat"],
|
|
84
|
+
"vi": ["trí tuệ nhân tạo", "hà nội du lịch"],
|
|
85
|
+
"th": ["ปัญญาประดิษฐ์", "ท่องเที่ยว กรุงเทพ"],
|
|
86
|
+
"hi": ["कृत्रिम बुद्धिमत्ता", "दिल्ली यात्रा"],
|
|
87
|
+
"he": ["בינה מלאכותית", "תל אביב טיול"],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ── 待测引擎:语言相关的(内容型),语言中立的(科学数据库)不测 ──────────
|
|
91
|
+
ENGINES = [
|
|
92
|
+
# 通用搜索
|
|
93
|
+
"wikipedia", "zh_wikipedia", "wikidata", "anysearch",
|
|
94
|
+
# 社区
|
|
95
|
+
"juejin", "qiita", "v2ex", "hackernews", "devto", "stackoverflow",
|
|
96
|
+
"zhihu_global", "hatena_bookmark",
|
|
97
|
+
# 新闻/媒体
|
|
98
|
+
"gdelt", "byted", "you", "em_global_news",
|
|
99
|
+
# 文化/书籍
|
|
100
|
+
"moegirl", "ndl", "gutenberg", "dnb", "open_library", "know_your_meme",
|
|
101
|
+
# 独立索引
|
|
102
|
+
"marginalia", "wiby", "lieu", "searchmysite",
|
|
103
|
+
# 中国源(对照组)
|
|
104
|
+
"bocha", "tavily", "exa",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def relevance(query: str, item: dict) -> float:
|
|
109
|
+
"""查询词元在结果中的命中率(0~1)。计算方式见模块 docstring。"""
|
|
110
|
+
blob = ((item.get("title") or "") + " " + (item.get("snippet") or "")).lower()
|
|
111
|
+
if not blob.strip():
|
|
112
|
+
return 0.0
|
|
113
|
+
# 拉丁词元
|
|
114
|
+
toks = [t for t in re.findall(r"[a-z]{3,}", query.lower())]
|
|
115
|
+
if toks:
|
|
116
|
+
return sum(1 for t in toks if t in blob) / len(toks)
|
|
117
|
+
# CJK / 日文假名
|
|
118
|
+
cjk = re.findall(r"[\u4e00-\u9fff\u3040-\u30ff]{2,}", query)
|
|
119
|
+
if cjk:
|
|
120
|
+
return sum(1 for c in cjk if c in blob) / len(cjk)
|
|
121
|
+
# 韩文
|
|
122
|
+
han = re.findall(r"[\uac00-\ud7af]{2,}", query)
|
|
123
|
+
if han:
|
|
124
|
+
return sum(1 for c in han if c in blob) / len(han)
|
|
125
|
+
# 西里尔 / 阿拉伯 / 天城文 / 希伯来:整词
|
|
126
|
+
words = [w for w in re.split(r"\s+", query) if len(w) >= 3]
|
|
127
|
+
if words:
|
|
128
|
+
return sum(1 for w in words if w in blob) / len(words)
|
|
129
|
+
return 0.0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _one_call(eng: str, q: str, n: int, timeout: float) -> dict:
|
|
133
|
+
"""单次 (引擎, 查询) 调用 → cell 结果。任何异常都收紧为 error 字段。"""
|
|
134
|
+
import engines as eng_mod
|
|
135
|
+
t0 = time.time()
|
|
136
|
+
try:
|
|
137
|
+
raw = eng_mod.search(q, eng, n=n, timeout=timeout) or []
|
|
138
|
+
err = None
|
|
139
|
+
except Exception as e:
|
|
140
|
+
raw, err = [], f"{type(e).__name__}"
|
|
141
|
+
# 过滤 error 占位条目:引擎失败时会返回 [{"error": "...", "source": ...}],
|
|
142
|
+
# 若不剔除,它会被当成「1 条结果、相关度 0」→ 误判为 noise。
|
|
143
|
+
# 实测影响:you/parallel 缺密钥时正是这种形态,早期版本把它们记成噪声而非故障。
|
|
144
|
+
results = [r for r in raw
|
|
145
|
+
if isinstance(r, dict) and not r.get("error")]
|
|
146
|
+
if raw and not results and not err:
|
|
147
|
+
err = str(raw[0].get("error"))[:60] if isinstance(raw[0], dict) else "error-only"
|
|
148
|
+
lat = round((time.time() - t0) * 1000)
|
|
149
|
+
if results:
|
|
150
|
+
sc = [relevance(q, x) for x in results]
|
|
151
|
+
rel = round(sum(sc) / len(sc), 3)
|
|
152
|
+
else:
|
|
153
|
+
rel = 0.0
|
|
154
|
+
return {"query": q, "count": len(results), "rel": rel,
|
|
155
|
+
"latency_ms": lat, "error": err}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _verdict(total: int, avg_rel: float) -> str:
|
|
159
|
+
if total == 0:
|
|
160
|
+
return "empty"
|
|
161
|
+
if avg_rel >= 0.5:
|
|
162
|
+
return "good"
|
|
163
|
+
if avg_rel >= 0.2:
|
|
164
|
+
return "partial"
|
|
165
|
+
return "noise"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def probe(engines: list[str], langs: list[str], n: int = 5,
|
|
169
|
+
timeout: float = 12.0, workers: int = 8,
|
|
170
|
+
out_path: str | None = None, quiet: bool = False) -> dict:
|
|
171
|
+
"""并发探测「语言 × 引擎」矩阵。
|
|
172
|
+
|
|
173
|
+
## 为什么必须并发(实测教训)
|
|
174
|
+
|
|
175
|
+
首版是三重串行循环:18 语言 × 28 引擎 × 2 查询 = 1008 次网络调用。
|
|
176
|
+
实测跑 35 分钟仅消耗 12 秒 CPU —— 即 99% 时间在等网络。
|
|
177
|
+
改为 8 并发后同样的工作量约 2-3 分钟。
|
|
178
|
+
|
|
179
|
+
## 增量写入文件
|
|
180
|
+
|
|
181
|
+
每完成一批(batch)就把当前矩阵写入 out_path。这样即使中途被中断,
|
|
182
|
+
已得数据仍可用——长任务不该「要么全有要么全无」。
|
|
183
|
+
"""
|
|
184
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
185
|
+
|
|
186
|
+
tasks: list[tuple[str, str, str]] = []
|
|
187
|
+
for lang in langs:
|
|
188
|
+
for q in (QUERIES.get(lang) or []):
|
|
189
|
+
for eng in engines:
|
|
190
|
+
tasks.append((lang, eng, q))
|
|
191
|
+
|
|
192
|
+
total_tasks = len(tasks)
|
|
193
|
+
if not quiet:
|
|
194
|
+
print(f"计划 {total_tasks} 次调用({len(langs)} 语言 × {len(engines)} 引擎 "
|
|
195
|
+
f"× 若干查询),并发 {workers}", flush=True)
|
|
196
|
+
|
|
197
|
+
# cells[(lang, eng)] = [cell, ...]
|
|
198
|
+
cells: dict[tuple[str, str], list[dict]] = {}
|
|
199
|
+
done = 0
|
|
200
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
201
|
+
futs = {ex.submit(_one_call, eng, q, n, timeout): (lang, eng, q)
|
|
202
|
+
for lang, eng, q in tasks}
|
|
203
|
+
for fut in as_completed(futs):
|
|
204
|
+
lang, eng, q = futs[fut]
|
|
205
|
+
try:
|
|
206
|
+
cell = fut.result()
|
|
207
|
+
except Exception as e: # 理论上 _one_call 不抛,双保险
|
|
208
|
+
cell = {"query": q, "count": 0, "rel": 0.0,
|
|
209
|
+
"latency_ms": 0, "error": f"future:{type(e).__name__}"}
|
|
210
|
+
cells.setdefault((lang, eng), []).append(cell)
|
|
211
|
+
done += 1
|
|
212
|
+
# 增量写入文件:每 50 次完成写一次
|
|
213
|
+
if out_path and done % 50 == 0:
|
|
214
|
+
_flush_matrix(cells, langs, engines, out_path,
|
|
215
|
+
{"langs": langs, "engines": engines, "n": n,
|
|
216
|
+
"progress": f"{done}/{total_tasks}",
|
|
217
|
+
"partial": True})
|
|
218
|
+
if not quiet and done % 100 == 0:
|
|
219
|
+
print(f" 进度 {done}/{total_tasks}", flush=True)
|
|
220
|
+
|
|
221
|
+
# 组装矩阵
|
|
222
|
+
matrix: dict[str, dict] = {}
|
|
223
|
+
for lang in langs:
|
|
224
|
+
if not (QUERIES.get(lang) or []):
|
|
225
|
+
continue
|
|
226
|
+
matrix[lang] = {}
|
|
227
|
+
for eng in engines:
|
|
228
|
+
cl = cells.get((lang, eng)) or []
|
|
229
|
+
if not cl:
|
|
230
|
+
continue
|
|
231
|
+
total = sum(c["count"] for c in cl)
|
|
232
|
+
avg_rel = (sum(c["rel"] * c["count"] for c in cl) / total
|
|
233
|
+
if total else 0.0)
|
|
234
|
+
matrix[lang][eng] = {
|
|
235
|
+
"total": total, "avg_rel": round(avg_rel, 3),
|
|
236
|
+
"verdict": _verdict(total, avg_rel), "cells": cl,
|
|
237
|
+
}
|
|
238
|
+
return matrix
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _flush_matrix(cells: dict, langs: list[str], engines: list[str],
|
|
242
|
+
out_path: str, meta: dict) -> None:
|
|
243
|
+
"""把当前 cells 组装成矩阵并写入文件(增量与最终共用)。"""
|
|
244
|
+
matrix: dict[str, dict] = {}
|
|
245
|
+
for lang in langs:
|
|
246
|
+
row: dict[str, dict] = {}
|
|
247
|
+
for eng in engines:
|
|
248
|
+
cl = cells.get((lang, eng)) or []
|
|
249
|
+
if not cl:
|
|
250
|
+
continue
|
|
251
|
+
total = sum(c["count"] for c in cl)
|
|
252
|
+
avg_rel = (sum(c["rel"] * c["count"] for c in cl) / total
|
|
253
|
+
if total else 0.0)
|
|
254
|
+
row[eng] = {"total": total, "avg_rel": round(avg_rel, 3),
|
|
255
|
+
"verdict": _verdict(total, avg_rel), "cells": cl}
|
|
256
|
+
if row:
|
|
257
|
+
matrix[lang] = row
|
|
258
|
+
out = {"matrix": matrix, "summary": summarize(matrix), "meta": meta}
|
|
259
|
+
Path(out_path).write_text(dumps_pretty(out), encoding="utf-8")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def summarize(matrix: dict) -> dict:
|
|
263
|
+
"""汇总:每语言可用引擎数 / 每引擎的语言覆盖。"""
|
|
264
|
+
per_lang, per_eng = {}, {}
|
|
265
|
+
for lang, row in matrix.items():
|
|
266
|
+
good = [e for e, m in row.items() if m["verdict"] == "good"]
|
|
267
|
+
partial = [e for e, m in row.items() if m["verdict"] == "partial"]
|
|
268
|
+
noise = [e for e, m in row.items() if m["verdict"] == "noise"]
|
|
269
|
+
per_lang[lang] = {"good": len(good), "partial": len(partial),
|
|
270
|
+
"noise": len(noise), "good_engines": good,
|
|
271
|
+
"noise_engines": noise}
|
|
272
|
+
for e, m in row.items():
|
|
273
|
+
per_eng.setdefault(e, {})[lang] = m["verdict"]
|
|
274
|
+
eng_summary = {}
|
|
275
|
+
for e, langs in per_eng.items():
|
|
276
|
+
eng_summary[e] = {
|
|
277
|
+
"good_langs": [l for l, v in langs.items() if v == "good"],
|
|
278
|
+
"noise_langs": [l for l, v in langs.items() if v == "noise"],
|
|
279
|
+
"good_count": sum(1 for v in langs.values() if v == "good"),
|
|
280
|
+
}
|
|
281
|
+
return {"per_lang": per_lang, "per_engine": eng_summary}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main() -> None:
|
|
285
|
+
ap = argparse.ArgumentParser(description="多语言 × 引擎能力矩阵实测")
|
|
286
|
+
ap.add_argument("--langs", default=",".join(QUERIES.keys()))
|
|
287
|
+
ap.add_argument("--engines", default=",".join(ENGINES))
|
|
288
|
+
ap.add_argument("-n", type=int, default=5)
|
|
289
|
+
ap.add_argument("--timeout", type=float, default=12.0)
|
|
290
|
+
ap.add_argument("--workers", type=int, default=8,
|
|
291
|
+
help="并发数(串行版实测 35 分钟只花 12s CPU,必须并发)")
|
|
292
|
+
ap.add_argument("--json", help="结果落盘路径(每 50 次调用增量写一次)")
|
|
293
|
+
args = ap.parse_args()
|
|
294
|
+
|
|
295
|
+
langs = [x.strip() for x in args.langs.split(",") if x.strip()]
|
|
296
|
+
engines = [x.strip() for x in args.engines.split(",") if x.strip()]
|
|
297
|
+
|
|
298
|
+
t0 = time.time()
|
|
299
|
+
matrix = probe(engines, langs, n=args.n, timeout=args.timeout,
|
|
300
|
+
workers=args.workers, out_path=args.json)
|
|
301
|
+
elapsed = round(time.time() - t0, 1)
|
|
302
|
+
summary = summarize(matrix)
|
|
303
|
+
|
|
304
|
+
if args.json:
|
|
305
|
+
_flush_matrix(
|
|
306
|
+
{(l, e): m["cells"] for l, row in matrix.items() for e, m in row.items()},
|
|
307
|
+
langs, engines, args.json,
|
|
308
|
+
{"langs": langs, "engines": engines, "n": args.n,
|
|
309
|
+
"elapsed_s": elapsed, "partial": False},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# 控制台摘要
|
|
313
|
+
print(f"\n耗时 {elapsed}s · {len(langs)} 语言 × {len(engines)} 引擎\n")
|
|
314
|
+
print(f"{'语言':<6}{'可用':>6}{'部分':>6}{'噪声':>6} 可用引擎")
|
|
315
|
+
print("-" * 72)
|
|
316
|
+
for lang, s in summary["per_lang"].items():
|
|
317
|
+
print(f"{lang:<6}{s['good']:>6}{s['partial']:>6}{s['noise']:>6} "
|
|
318
|
+
f"{', '.join(s['good_engines'][:5])}")
|
|
319
|
+
print()
|
|
320
|
+
print(f"{'引擎':<18}{'良好语言数':>10} 良好语言")
|
|
321
|
+
print("-" * 72)
|
|
322
|
+
ranked = sorted(summary["per_engine"].items(),
|
|
323
|
+
key=lambda x: -x[1]["good_count"])
|
|
324
|
+
for e, s in ranked:
|
|
325
|
+
print(f"{e:<18}{s['good_count']:>10} {', '.join(s['good_langs'][:8])}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == "__main__":
|
|
329
|
+
main()
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lang_pref.py — 搜索语言偏好:默认中英 + 系统语言 + 使用习惯
|
|
3
|
+
|
|
4
|
+
像系统软件的「界面语言」与「内容语言」分层一样,搜索侧拆成:
|
|
5
|
+
|
|
6
|
+
1. 本轮查询语言(强信号)—— 永远驱动 setlang / 路由 / 引擎选择
|
|
7
|
+
2. 用户使用习惯(滑动窗口)—— 持续使用某语种后提升该语种偏好
|
|
8
|
+
3. 系统 locale —— 冷启动种子(macOS/Linux LANG、LC_ALL)
|
|
9
|
+
4. 内置基线 zh + en —— 永不剔除,中英双轨覆盖全球主流量
|
|
10
|
+
|
|
11
|
+
原则:
|
|
12
|
+
- 强查询信号(zh/ja/ko/非拉丁等)以查询为准,习惯/系统只作补充与观测
|
|
13
|
+
- 弱信号(mixed / other / 空)才用 prefer_langs[0] 驱动引擎语言参数
|
|
14
|
+
- 中英基线始终在 prefer 列表中,习惯学到 ja 也不会丢掉 zh/en
|
|
15
|
+
- 纯本地、仅 stdlib;状态写 <状态目录>/lang_habit.json(由 argo_paths 派生)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Optional
|
|
27
|
+
|
|
28
|
+
# ── 路径 / 常量 ──────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def _state_dir() -> Path:
|
|
31
|
+
"""状态目录(惰性派生,支持 ARGO_STATE_DIR 覆盖)。"""
|
|
32
|
+
import argo_paths
|
|
33
|
+
return argo_paths.ensure_state_dir()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
STATE_DIR = _state_dir() # 兼容旧引用
|
|
37
|
+
STATE_PATH = STATE_DIR / "lang_habit.json"
|
|
38
|
+
|
|
39
|
+
# 全球默认:中英双语基线(永不剔除)
|
|
40
|
+
BASELINE_LANGS: tuple[str, ...] = ("zh", "en")
|
|
41
|
+
|
|
42
|
+
# 习惯生效阈值:至少 N 条样本且主导语种占比 ≥ 阈值
|
|
43
|
+
HABIT_MIN_SAMPLES = 5
|
|
44
|
+
HABIT_MIN_RATIO = 0.55
|
|
45
|
+
HABIT_WINDOW = 80 # 滑动窗口最大事件数
|
|
46
|
+
|
|
47
|
+
# 与 lang_detect 保持一致的可学习标签(排除 mixed/other 噪声)
|
|
48
|
+
_TRACKABLE = frozenset({
|
|
49
|
+
"zh", "en", "ja", "ko", "latin", "cyrillic", "thai",
|
|
50
|
+
"arabic", "hebrew", "greek", "devanagari",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# 弱信号:引擎语言参数可回落到偏好语
|
|
54
|
+
WEAK_QUERY_LANGS = frozenset({"mixed", "other", ""})
|
|
55
|
+
|
|
56
|
+
# locale 前缀 → argo 语言标签
|
|
57
|
+
_LOCALE_PREFIX: list[tuple[str, str]] = [
|
|
58
|
+
("zh", "zh"),
|
|
59
|
+
("ja", "ja"),
|
|
60
|
+
("ko", "ko"),
|
|
61
|
+
("en", "en"),
|
|
62
|
+
("ru", "cyrillic"),
|
|
63
|
+
("uk", "cyrillic"),
|
|
64
|
+
("bg", "cyrillic"),
|
|
65
|
+
("th", "thai"),
|
|
66
|
+
("ar", "arabic"),
|
|
67
|
+
("he", "hebrew"),
|
|
68
|
+
("iw", "hebrew"),
|
|
69
|
+
("el", "greek"),
|
|
70
|
+
("hi", "devanagari"),
|
|
71
|
+
("mr", "devanagari"),
|
|
72
|
+
("ne", "devanagari"),
|
|
73
|
+
# 西欧带变音:归 latin(与 detect_language 一致)
|
|
74
|
+
("fr", "latin"),
|
|
75
|
+
("de", "latin"),
|
|
76
|
+
("es", "latin"),
|
|
77
|
+
("it", "latin"),
|
|
78
|
+
("pt", "latin"),
|
|
79
|
+
("nl", "latin"),
|
|
80
|
+
("sv", "latin"),
|
|
81
|
+
("pl", "latin"),
|
|
82
|
+
("tr", "latin"),
|
|
83
|
+
("vi", "latin"),
|
|
84
|
+
("id", "latin"),
|
|
85
|
+
("ms", "latin"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
_lock = threading.Lock()
|
|
89
|
+
_cache: dict[str, Any] | None = None
|
|
90
|
+
_cache_mtime: float = 0.0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── 系统 locale ──────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def _raw_locale() -> str:
|
|
96
|
+
"""读取进程环境语言标签(不调用 setlocale,避免改全局状态)。"""
|
|
97
|
+
for key in ("LC_ALL", "LC_MESSAGES", "LANG"):
|
|
98
|
+
val = (os.environ.get(key) or "").strip()
|
|
99
|
+
if val and val not in ("C", "POSIX"):
|
|
100
|
+
return val
|
|
101
|
+
try:
|
|
102
|
+
import locale
|
|
103
|
+
loc = locale.getlocale()
|
|
104
|
+
if loc and loc[0]:
|
|
105
|
+
return loc[0]
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def system_lang(raw: str | None = None) -> str:
|
|
112
|
+
"""系统语言 → argo 标签;无法判定时返回空串(不冒充 en)。"""
|
|
113
|
+
s = (raw if raw is not None else _raw_locale()).strip()
|
|
114
|
+
if not s:
|
|
115
|
+
return ""
|
|
116
|
+
# zh_CN.UTF-8 / ja-JP / en_US → 取前缀
|
|
117
|
+
s = s.replace("-", "_")
|
|
118
|
+
m = re.match(r"^([A-Za-z]{2,3})", s)
|
|
119
|
+
if not m:
|
|
120
|
+
return ""
|
|
121
|
+
prefix = m.group(1).lower()
|
|
122
|
+
for p, tag in _LOCALE_PREFIX:
|
|
123
|
+
if prefix == p:
|
|
124
|
+
return tag
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ── 习惯持久化 ───────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
def _empty_state() -> dict[str, Any]:
|
|
131
|
+
return {"version": 1, "events": [], "updated_at": 0.0}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _load_state() -> dict[str, Any]:
|
|
135
|
+
global _cache, _cache_mtime
|
|
136
|
+
with _lock:
|
|
137
|
+
try:
|
|
138
|
+
if STATE_PATH.exists():
|
|
139
|
+
mtime = STATE_PATH.stat().st_mtime
|
|
140
|
+
if _cache is not None and mtime == _cache_mtime:
|
|
141
|
+
return _cache
|
|
142
|
+
data = json.loads(STATE_PATH.read_text(encoding="utf-8"))
|
|
143
|
+
if not isinstance(data, dict):
|
|
144
|
+
data = _empty_state()
|
|
145
|
+
data.setdefault("events", [])
|
|
146
|
+
data.setdefault("version", 1)
|
|
147
|
+
_cache = data
|
|
148
|
+
_cache_mtime = mtime
|
|
149
|
+
return data
|
|
150
|
+
except (OSError, json.JSONDecodeError, TypeError):
|
|
151
|
+
pass
|
|
152
|
+
_cache = _empty_state()
|
|
153
|
+
_cache_mtime = 0.0
|
|
154
|
+
return _cache
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _save_state(state: dict[str, Any]) -> None:
|
|
158
|
+
global _cache, _cache_mtime
|
|
159
|
+
with _lock:
|
|
160
|
+
try:
|
|
161
|
+
state["updated_at"] = time.time()
|
|
162
|
+
# 原子写走唯一来源(唯一 tmp 名)——旧实现固定 `.tmp` 名,
|
|
163
|
+
# 并发进程会互相搬走临时文件导致写失败。
|
|
164
|
+
import argo_paths as _paths
|
|
165
|
+
_paths.atomic_write_json(STATE_PATH, state, indent=None)
|
|
166
|
+
_cache = state
|
|
167
|
+
_cache_mtime = STATE_PATH.stat().st_mtime
|
|
168
|
+
except OSError:
|
|
169
|
+
pass # 偏好写失败不阻断搜索
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def record_query_lang(lang: str) -> None:
|
|
173
|
+
"""记录一次查询主语言(仅可追踪标签;mixed/other 忽略)。"""
|
|
174
|
+
if not lang or lang not in _TRACKABLE:
|
|
175
|
+
return
|
|
176
|
+
state = _load_state()
|
|
177
|
+
events = list(state.get("events") or [])
|
|
178
|
+
events.append({"lang": lang, "ts": time.time()})
|
|
179
|
+
if len(events) > HABIT_WINDOW:
|
|
180
|
+
events = events[-HABIT_WINDOW:]
|
|
181
|
+
state["events"] = events
|
|
182
|
+
_save_state(state)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def dominant_habit(
|
|
186
|
+
min_samples: int = HABIT_MIN_SAMPLES,
|
|
187
|
+
min_ratio: float = HABIT_MIN_RATIO,
|
|
188
|
+
events: list[dict[str, Any]] | None = None,
|
|
189
|
+
) -> Optional[str]:
|
|
190
|
+
"""滑动窗口主导语种;样本不足或不够集中时返回 None。"""
|
|
191
|
+
if events is None:
|
|
192
|
+
events = list((_load_state().get("events") or []))
|
|
193
|
+
if len(events) < min_samples:
|
|
194
|
+
return None
|
|
195
|
+
counts: dict[str, int] = {}
|
|
196
|
+
for e in events:
|
|
197
|
+
lang = e.get("lang") if isinstance(e, dict) else None
|
|
198
|
+
if lang in _TRACKABLE:
|
|
199
|
+
counts[lang] = counts.get(lang, 0) + 1
|
|
200
|
+
if not counts:
|
|
201
|
+
return None
|
|
202
|
+
top_lang, top_n = max(counts.items(), key=lambda x: x[1])
|
|
203
|
+
total = sum(counts.values())
|
|
204
|
+
if total < min_samples or top_n / total < min_ratio:
|
|
205
|
+
return None
|
|
206
|
+
return top_lang
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def habit_counts() -> dict[str, int]:
|
|
210
|
+
"""调试/观测:各语种计数。"""
|
|
211
|
+
counts: dict[str, int] = {}
|
|
212
|
+
for e in (_load_state().get("events") or []):
|
|
213
|
+
lang = e.get("lang") if isinstance(e, dict) else None
|
|
214
|
+
if lang in _TRACKABLE:
|
|
215
|
+
counts[lang] = counts.get(lang, 0) + 1
|
|
216
|
+
return counts
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── 偏好解析 ─────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def prefer_langs(
|
|
222
|
+
query_lang: str | None = None,
|
|
223
|
+
*,
|
|
224
|
+
system: str | None = None,
|
|
225
|
+
habit: str | None = None,
|
|
226
|
+
use_live_habit: bool = True,
|
|
227
|
+
) -> list[str]:
|
|
228
|
+
"""有序语言偏好列表(去重,基线 zh+en 垫底)。
|
|
229
|
+
|
|
230
|
+
顺序:本轮强查询语 → 习惯主导语 → 系统语言 → 中英基线。
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
query_lang: 本轮 detect_language 结果;弱信号不前置。
|
|
234
|
+
system: 注入系统语(测试用);None 则现场读 locale。
|
|
235
|
+
habit: 注入习惯语(测试用);None 且 use_live_habit 时读持久化。
|
|
236
|
+
use_live_habit: 是否读取磁盘习惯(单测可关)。
|
|
237
|
+
"""
|
|
238
|
+
ordered: list[str] = []
|
|
239
|
+
|
|
240
|
+
def _push(tag: str | None) -> None:
|
|
241
|
+
if tag and tag not in ordered and tag in _TRACKABLE:
|
|
242
|
+
ordered.append(tag)
|
|
243
|
+
|
|
244
|
+
ql = (query_lang or "").strip()
|
|
245
|
+
# 强查询信号前置;弱信号留给习惯/系统
|
|
246
|
+
if ql and ql not in WEAK_QUERY_LANGS:
|
|
247
|
+
_push(ql)
|
|
248
|
+
|
|
249
|
+
if habit is not None:
|
|
250
|
+
_push(habit)
|
|
251
|
+
elif use_live_habit:
|
|
252
|
+
_push(dominant_habit())
|
|
253
|
+
|
|
254
|
+
sys_tag = system if system is not None else system_lang()
|
|
255
|
+
_push(sys_tag)
|
|
256
|
+
|
|
257
|
+
for b in BASELINE_LANGS:
|
|
258
|
+
_push(b)
|
|
259
|
+
|
|
260
|
+
# 极端保底:若全部过滤空,仍返回中英
|
|
261
|
+
return ordered or list(BASELINE_LANGS)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def effective_engine_lang(query_lang: str, preferred: list[str] | None = None) -> str:
|
|
265
|
+
"""决定引擎 setlang/hl 用的语言标签。
|
|
266
|
+
|
|
267
|
+
强查询语 → 原样;弱信号 → prefer_langs 首项(默认 en,因基线含 en)。
|
|
268
|
+
"""
|
|
269
|
+
ql = (query_lang or "").strip()
|
|
270
|
+
if ql and ql not in WEAK_QUERY_LANGS:
|
|
271
|
+
return ql
|
|
272
|
+
prefs = preferred if preferred is not None else prefer_langs(query_lang=ql)
|
|
273
|
+
for p in prefs:
|
|
274
|
+
if p in _TRACKABLE:
|
|
275
|
+
return p
|
|
276
|
+
return "en"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def is_weak_query_lang(lang: str) -> bool:
|
|
280
|
+
return (lang or "") in WEAK_QUERY_LANGS
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def lang_pref_snapshot(query_lang: str | None = None) -> dict[str, Any]:
|
|
284
|
+
"""供 search 结果顶层观测字段。"""
|
|
285
|
+
sys_tag = system_lang()
|
|
286
|
+
habit = dominant_habit()
|
|
287
|
+
prefs = prefer_langs(query_lang=query_lang, system=sys_tag, habit=habit,
|
|
288
|
+
use_live_habit=False)
|
|
289
|
+
return {
|
|
290
|
+
"query_lang": query_lang or "",
|
|
291
|
+
"system_lang": sys_tag,
|
|
292
|
+
"habit_lang": habit or "",
|
|
293
|
+
"prefer_langs": prefs,
|
|
294
|
+
"baseline": list(BASELINE_LANGS),
|
|
295
|
+
"engine_lang": effective_engine_lang(query_lang or "", prefs),
|
|
296
|
+
"habit_counts": habit_counts(),
|
|
297
|
+
"habit_samples": sum(habit_counts().values()),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def reset_habit_for_tests() -> None:
|
|
302
|
+
"""测试用:清空内存与磁盘习惯。"""
|
|
303
|
+
global _cache, _cache_mtime
|
|
304
|
+
with _lock:
|
|
305
|
+
_cache = _empty_state()
|
|
306
|
+
_cache_mtime = 0.0
|
|
307
|
+
try:
|
|
308
|
+
if STATE_PATH.exists():
|
|
309
|
+
STATE_PATH.unlink()
|
|
310
|
+
except OSError:
|
|
311
|
+
pass
|