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,235 @@
|
|
|
1
|
+
"""jump_resolver — 不透明跳转壳解析(baidu/sogou/so /link 30x Location)。
|
|
2
|
+
|
|
3
|
+
baidu/sogou 结果链接是 token 型跳转(与 DDG uddg 的可离线还原不同),聚合层
|
|
4
|
+
P0(evidence.is_serp_or_jump_url)把这类 URL 整批滤掉——引擎不解析就永远
|
|
5
|
+
0 条。本模块提供引擎级解析:单跳请求读 Location,失败返回空串由调用方丢弃
|
|
6
|
+
(与 _unwrap_ddg_link 同哲学:不可核验的跳转壳不进结果)。
|
|
7
|
+
|
|
8
|
+
两条落点形态(2026-10-06 实测补第二类):
|
|
9
|
+
- 30x → 直接读 Location(baidu /link 之类);
|
|
10
|
+
- 200 + 短页 → 页面里 window.location.replace("真实URL") 一类 JS 跳转。
|
|
11
|
+
搜狗对浏览器指纹客户端走这条(~234 字节小页);对裸指纹/无会话客户端
|
|
12
|
+
302 弹回首页 Location: /,同源弹回一律不救回。
|
|
13
|
+
|
|
14
|
+
第三类(2026-10-17 补):Bing `ck/a?...&u=a1<base64url>` 把落点编码在参数里,
|
|
15
|
+
**不需要任何网络请求**即可还原。此前 local_bing 的英文结果全被聚合层当
|
|
16
|
+
搜索页滤掉(近 7 天 271 次调用、40.2% 空结果),而它本可零成本变成真链。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import html as _html
|
|
23
|
+
import re
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import urllib.request
|
|
26
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
27
|
+
|
|
28
|
+
JUMP_MARKERS = ("baidu.com/link", "baidu.com/baidu.php",
|
|
29
|
+
"sogou.com/link", "so.com/link",
|
|
30
|
+
"bing.com/ck/a")
|
|
31
|
+
|
|
32
|
+
# 跳转页里的 JS/meta 落点(按优先级排列,先命中先赢)
|
|
33
|
+
_JS_TARGET_RES = (
|
|
34
|
+
re.compile(r"window\.location\.replace\(\s*[\"']([^\"']+)[\"']\s*\)"),
|
|
35
|
+
re.compile(r"window\.location\.href\s*=\s*[\"']([^\"']+)[\"']"),
|
|
36
|
+
re.compile(r"location\.href\s*=\s*[\"']([^\"']+)[\"']"),
|
|
37
|
+
re.compile(r"location\.replace\(\s*[\"']([^\"']+)[\"']\s*\)"),
|
|
38
|
+
re.compile(r"""<meta[^>]+http-equiv=["']refresh["'][^>]+
|
|
39
|
+
content=["'][^"']*url=([^"';]+)""", re.IGNORECASE | re.VERBOSE),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# 200 响应只有落在这么短以内才当跳转页读:真实正文页不该进来白耗解析
|
|
43
|
+
_MAX_REDIRECT_PAGE_BYTES = 8192
|
|
44
|
+
|
|
45
|
+
# 批量解析的总量与并发上限:最坏墙钟 = ceil(cap/workers) × timeout
|
|
46
|
+
_BATCH_CAP = 16
|
|
47
|
+
_BATCH_WORKERS = 8
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_jump_url(u: str) -> bool:
|
|
51
|
+
return any(m in (u or "") for m in JUMP_MARKERS)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# 可离线还原的落点参数:Bing 把目标 URL 放在 `u=a1<base64url>` 里。
|
|
55
|
+
_BING_CK_HOSTS = ("bing.com", "www.bing.com", "cn.bing.com")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _unwrap_bing(url: str) -> str:
|
|
59
|
+
"""Bing `ck/a` 跳转壳 → 真实 URL;不适用或解码失败返回空串。
|
|
60
|
+
|
|
61
|
+
`u` 的值是 `a1` + URL-safe base64(无 padding)。解码后必须是 http(s)
|
|
62
|
+
绝对地址,否则视为不可用(与 _is_usable_target 同哲学)。
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
parsed = urllib.parse.urlsplit(url)
|
|
66
|
+
if parsed.netloc.lower() not in _BING_CK_HOSTS:
|
|
67
|
+
return ""
|
|
68
|
+
if not parsed.path.startswith("/ck/"):
|
|
69
|
+
return ""
|
|
70
|
+
raw = (urllib.parse.parse_qs(parsed.query).get("u") or [""])[0]
|
|
71
|
+
if not isinstance(raw, str) or not raw:
|
|
72
|
+
return ""
|
|
73
|
+
if raw.startswith("a1"):
|
|
74
|
+
raw = raw[2:]
|
|
75
|
+
padded = raw + "=" * (-len(raw) % 4)
|
|
76
|
+
target = base64.urlsafe_b64decode(padded.encode("ascii")).decode(
|
|
77
|
+
"utf-8", "replace").strip()
|
|
78
|
+
if not target.lower().startswith(("http://", "https://")):
|
|
79
|
+
return ""
|
|
80
|
+
# 同源弹回(目标又落回 bing 搜索页)不救回,避免把壳当答案。
|
|
81
|
+
if _is_usable_target(target, url):
|
|
82
|
+
return target
|
|
83
|
+
return ""
|
|
84
|
+
except Exception:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def unwrap_offline_url(url: str) -> str:
|
|
89
|
+
"""不需要网络的跳转壳还原;不适用/失败返回空串。
|
|
90
|
+
|
|
91
|
+
调用方可以在解析阶段无条件调用:非跳转 URL 直接返回空串,成本只有
|
|
92
|
+
`in` 判断。优先于任何 30x/Location 网络解析——Bing 的落点本来就写在
|
|
93
|
+
参数里,发请求纯属浪费。
|
|
94
|
+
"""
|
|
95
|
+
u = url or ""
|
|
96
|
+
if not u:
|
|
97
|
+
return ""
|
|
98
|
+
if "bing.com/ck/" in u:
|
|
99
|
+
return _unwrap_bing(u)
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _extract_js_target(body: str) -> str:
|
|
104
|
+
"""从 JS/meta 跳转页提取落点;没有则空串。"""
|
|
105
|
+
for rx in _JS_TARGET_RES:
|
|
106
|
+
m = rx.search(body)
|
|
107
|
+
if m:
|
|
108
|
+
return _html.unescape(m.group(1).strip())
|
|
109
|
+
return ""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_usable_target(target: str, source_url: str) -> bool:
|
|
113
|
+
"""落点基本可用性:非绝对 URL / 同源弹回(搜狗无会话时 Location: /,
|
|
114
|
+
经 urljoin 后是同 origin 的首页)不算可用落点。"""
|
|
115
|
+
if not target or not target.lower().startswith(("http://", "https://")):
|
|
116
|
+
return False
|
|
117
|
+
try:
|
|
118
|
+
src_origin = urllib.parse.urlsplit(source_url).netloc.lower()
|
|
119
|
+
tgt_origin = urllib.parse.urlsplit(target).netloc.lower()
|
|
120
|
+
except ValueError:
|
|
121
|
+
return False
|
|
122
|
+
return bool(tgt_origin) and tgt_origin != src_origin
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _fetch_no_redirect(url: str, timeout: float,
|
|
126
|
+
profiles: list[str]) -> tuple[int, dict[str, str], str]:
|
|
127
|
+
"""请求且不跟随跳转,返回 (status, headers, body)。
|
|
128
|
+
|
|
129
|
+
curl_cffi 缺席时回落 urllib(只覆盖 302 场景,JS 页提取不了)。
|
|
130
|
+
网络层异常向上抛,由调用方统一 fail-open。
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
from curl_cffi import requests as cr
|
|
134
|
+
except ImportError:
|
|
135
|
+
req = urllib.request.Request(url, headers={
|
|
136
|
+
"User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
137
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
138
|
+
"Chrome/126.0.0.0 Safari/537.36"),
|
|
139
|
+
})
|
|
140
|
+
opener = urllib.request.build_opener(_NoRedirectHandler)
|
|
141
|
+
try:
|
|
142
|
+
with opener.open(req, timeout=timeout) as resp:
|
|
143
|
+
return resp.status, dict(resp.headers), ""
|
|
144
|
+
except urllib.error.HTTPError as e:
|
|
145
|
+
return e.code, dict(e.headers or {}), ""
|
|
146
|
+
|
|
147
|
+
proxies = None
|
|
148
|
+
try:
|
|
149
|
+
from net_proxy import resolve_proxy
|
|
150
|
+
purl = resolve_proxy(url)
|
|
151
|
+
if purl:
|
|
152
|
+
proxies = {"http": purl, "https": purl}
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
last_err: Exception | None = None
|
|
156
|
+
for fp in profiles:
|
|
157
|
+
try:
|
|
158
|
+
resp = cr.get(url, timeout=timeout, allow_redirects=False,
|
|
159
|
+
proxies=proxies, impersonate=fp)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
last_err = e
|
|
162
|
+
continue
|
|
163
|
+
status = int(resp.status_code)
|
|
164
|
+
body = ""
|
|
165
|
+
if status == 200 and len(resp.content or b"") <= _MAX_REDIRECT_PAGE_BYTES:
|
|
166
|
+
body = resp.text or ""
|
|
167
|
+
return status, dict(resp.headers), body
|
|
168
|
+
if last_err is not None:
|
|
169
|
+
raise last_err
|
|
170
|
+
return 0, {}, ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
174
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
175
|
+
return None # 拦下 3xx,把 Location 留给调用方
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _resolve_core(url: str, timeout: float, profiles: list[str]) -> str:
|
|
179
|
+
"""单条解析本体:30x 读 Location,200 短页提取 JS 落点;失败空串。"""
|
|
180
|
+
try:
|
|
181
|
+
status, headers, body = _fetch_no_redirect(url, timeout, profiles)
|
|
182
|
+
except Exception:
|
|
183
|
+
return ""
|
|
184
|
+
try:
|
|
185
|
+
if 300 <= status < 400:
|
|
186
|
+
target = urllib.parse.urljoin(url, (headers.get("Location") or "").strip())
|
|
187
|
+
elif status == 200 and body:
|
|
188
|
+
target = urllib.parse.urljoin(url, _extract_js_target(body))
|
|
189
|
+
else:
|
|
190
|
+
return ""
|
|
191
|
+
except Exception:
|
|
192
|
+
return ""
|
|
193
|
+
return target if _is_usable_target(target, url) else ""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def resolve_jump_url(url: str, profiles: list[str] | None = None,
|
|
197
|
+
timeout: float = 8.0) -> str:
|
|
198
|
+
"""跳转壳 → 真实 URL(离线参数优先,其次单跳 Location / JS 落点)。
|
|
199
|
+
|
|
200
|
+
失败返回空串。离线可还原的壳不发网络请求。
|
|
201
|
+
"""
|
|
202
|
+
offline = unwrap_offline_url(url)
|
|
203
|
+
if offline:
|
|
204
|
+
return offline
|
|
205
|
+
return _resolve_core(url, timeout, profiles or ["chrome131"])
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def resolve_jump_urls(urls: list[str], timeout: float = 2.5,
|
|
209
|
+
profiles: list[str] | None = None,
|
|
210
|
+
acceptable=None) -> dict[str, str]:
|
|
211
|
+
"""批量并行解析,返回 {原URL: 落点URL}(只含解析成功的)。
|
|
212
|
+
|
|
213
|
+
`acceptable`:可选落点复核回调(主链传 serp_guard 判据——落点若仍是
|
|
214
|
+
搜索页/跳转页则不救回);缺省只做同源弹回检查。超过 _BATCH_CAP 的
|
|
215
|
+
部分不解析(调用方按重要性排序给,前头的优先保住);单条失败只丢
|
|
216
|
+
自己那条,不影响其余(fail-open)。
|
|
217
|
+
"""
|
|
218
|
+
uniq = [u for u in dict.fromkeys(urls) if u][:_BATCH_CAP]
|
|
219
|
+
if not uniq:
|
|
220
|
+
return {}
|
|
221
|
+
profiles = profiles or ["chrome131"]
|
|
222
|
+
workers = max(1, min(_BATCH_WORKERS, len(uniq)))
|
|
223
|
+
with ThreadPoolExecutor(max_workers=workers,
|
|
224
|
+
thread_name_prefix="jump-resolve") as ex:
|
|
225
|
+
targets = dict(zip(uniq, ex.map(lambda u: _resolve_core(u, timeout,
|
|
226
|
+
profiles),
|
|
227
|
+
uniq)))
|
|
228
|
+
out: dict[str, str] = {}
|
|
229
|
+
for src, target in targets.items():
|
|
230
|
+
if not target:
|
|
231
|
+
continue
|
|
232
|
+
if acceptable is not None and not acceptable(target):
|
|
233
|
+
continue
|
|
234
|
+
out[src] = target
|
|
235
|
+
return out
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lang_capability.py — 语言能力画像:让路由按「引擎在某语言下真的行」来选。
|
|
3
|
+
|
|
4
|
+
## 问题(C 阶段实测)
|
|
5
|
+
|
|
6
|
+
18 语言 × 29 引擎矩阵(`scripts/lang_matrix_probe.py`)暴露两件事:
|
|
7
|
+
|
|
8
|
+
1. **语言覆盖极不均衡**:西班牙语 12 个可用引擎,泰语只有 3 个;
|
|
9
|
+
日语只有 5 个——而 argo 明明有 Qiita / NDL / 萌娘百科等日文资源。
|
|
10
|
+
2. **「可用」不等于「被用上」**:路由此前只看 `coverage` 字段(157 个引擎里
|
|
11
|
+
109 个根本没声明语言),于是「引擎在某语言下真的有内容」这个事实
|
|
12
|
+
完全没进入决策。
|
|
13
|
+
|
|
14
|
+
## 机制
|
|
15
|
+
|
|
16
|
+
把矩阵实测结果固化为**能力画像**(`data/lang_matrix/lang_capability.json`),
|
|
17
|
+
路由据此对候选引擎打语言适配分。
|
|
18
|
+
|
|
19
|
+
设计取舍——**建议而非硬过滤**:
|
|
20
|
+
· 硬过滤的风险:矩阵是单次实测,含网络抖动与查询抽样偏差;
|
|
21
|
+
一旦硬砍,某次抖动会让整门语言失去检索能力。
|
|
22
|
+
· 加权则两头兼顾:能力强的引擎优先,未知的保持原样(不惩罚),
|
|
23
|
+
实测为噪声的降权(不禁止)。
|
|
24
|
+
· 画像缺失/过期时**完全退化为原行为**——这是安全保底。
|
|
25
|
+
|
|
26
|
+
## 数据来源与新鲜度
|
|
27
|
+
|
|
28
|
+
画像由 `lang_matrix_probe.py` 生成。调用方不应假设它永远存在;
|
|
29
|
+
`available()` 返回 False 时所有查询按原权重走。
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import time
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
_PROFILE_PATH = Path(__file__).resolve().parent.parent / "data" / "lang_matrix" / "lang_capability.json"
|
|
40
|
+
|
|
41
|
+
# 画像最大可接受年龄(秒)。超过则视为过期——语言能力会随上游改版变化,
|
|
42
|
+
# 陈年数据不该继续影响路由。30 天是「够用且不至于陈旧」的折中。
|
|
43
|
+
MAX_AGE_S = 30 * 24 * 3600
|
|
44
|
+
|
|
45
|
+
# 评分权重
|
|
46
|
+
BOOST_GOOD = 1.15 # 实测该语言下良好 → 提权
|
|
47
|
+
PENALTY_NOISE = 0.85 # 实测该语言下噪声 → 降权
|
|
48
|
+
NEUTRAL = 1.0 # 无数据 → 不动
|
|
49
|
+
|
|
50
|
+
_cache: dict[str, Any] = {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load() -> dict[str, Any] | None:
|
|
54
|
+
"""读能力画像(带进程内缓存)。缺失/损坏/过期返回 None。"""
|
|
55
|
+
if "profile" in _cache:
|
|
56
|
+
return _cache["profile"]
|
|
57
|
+
profile = None
|
|
58
|
+
try:
|
|
59
|
+
st = _PROFILE_PATH.stat()
|
|
60
|
+
if time.time() - st.st_mtime <= MAX_AGE_S:
|
|
61
|
+
data = json.loads(_PROFILE_PATH.read_text(encoding="utf-8"))
|
|
62
|
+
if isinstance(data, dict):
|
|
63
|
+
profile = data
|
|
64
|
+
except Exception:
|
|
65
|
+
profile = None
|
|
66
|
+
_cache["profile"] = profile
|
|
67
|
+
return profile
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def available() -> bool:
|
|
71
|
+
"""画像是否可用。False 时调用方应完全按原行为处理。"""
|
|
72
|
+
return _load() is not None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def engines_for_lang(lang: str) -> set[str]:
|
|
76
|
+
"""该语言下实测良好的引擎集合(画像缺失时返回空集)。"""
|
|
77
|
+
p = _load()
|
|
78
|
+
if not p:
|
|
79
|
+
return set()
|
|
80
|
+
langs = p.get("lang_engines") or {}
|
|
81
|
+
# 语系级标签归一:detect_language 可能给 latin/cyrillic,
|
|
82
|
+
# 而画像按具体语种建(de/fr/ru…)。此时合并该语系全部成员。
|
|
83
|
+
members = _expand_lang(lang)
|
|
84
|
+
out: set[str] = set()
|
|
85
|
+
for m in members:
|
|
86
|
+
for e in (langs.get(m) or []):
|
|
87
|
+
out.add(e)
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def noise_engines_for_lang(lang: str) -> set[str]:
|
|
92
|
+
"""该语言下实测为噪声的引擎(用于降权)。"""
|
|
93
|
+
p = _load()
|
|
94
|
+
if not p:
|
|
95
|
+
return set()
|
|
96
|
+
per_lang = ((p.get("summary") or {}).get("per_lang")) or {}
|
|
97
|
+
members = _expand_lang(lang)
|
|
98
|
+
out: set[str] = set()
|
|
99
|
+
for m in members:
|
|
100
|
+
for e in (per_lang.get(m, {}).get("noise_engines") or []):
|
|
101
|
+
out.add(e)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_FAMILY = {
|
|
106
|
+
"latin": ("en", "de", "fr", "es", "pt", "it", "nl", "pl", "tr", "vi"),
|
|
107
|
+
"cyrillic": ("ru",),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _expand_lang(lang: str) -> tuple[str, ...]:
|
|
112
|
+
"""**仅当输入是语系标签**时展开为具体语种成员;具体语种原样返回。
|
|
113
|
+
|
|
114
|
+
为什么不对具体语种做同族扩展(实测教训):
|
|
115
|
+
detect_language 对语言明确的查询返回具体标签(zh/ja/de/ru…),
|
|
116
|
+
只有判不出来时才返回语系标签(latin/cyrillic)。而**同语系的
|
|
117
|
+
语言能力并不通用**——实测 zh_wikipedia 擅长中文(zh/en/fr/es…)
|
|
118
|
+
却完全不胜任日语;ndl 擅长日语却不擅长中文。若把 ja 展开成
|
|
119
|
+
(zh, ja),中文引擎的能力会被误算到日语头上。
|
|
120
|
+
|
|
121
|
+
故:具体语种 → 精确匹配;语系标签 → 展开(因为此时确实不知道
|
|
122
|
+
是哪种语言,只能取该语系并集作为参考)。
|
|
123
|
+
"""
|
|
124
|
+
if not lang:
|
|
125
|
+
return ()
|
|
126
|
+
if lang in _FAMILY:
|
|
127
|
+
return _FAMILY[lang]
|
|
128
|
+
return (lang,)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def score_adjust(engine: str, lang: str) -> float:
|
|
132
|
+
"""返回该引擎在该语言下的权重系数(1.0 表示不调整)。
|
|
133
|
+
|
|
134
|
+
调用方把返回值乘到引擎原有评分上即可。画像缺失时恒为 1.0。
|
|
135
|
+
"""
|
|
136
|
+
if not engine or not lang:
|
|
137
|
+
return NEUTRAL
|
|
138
|
+
p = _load()
|
|
139
|
+
if not p:
|
|
140
|
+
return NEUTRAL
|
|
141
|
+
if engine in engines_for_lang(lang):
|
|
142
|
+
return BOOST_GOOD
|
|
143
|
+
if engine in noise_engines_for_lang(lang):
|
|
144
|
+
return PENALTY_NOISE
|
|
145
|
+
return NEUTRAL
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def reload() -> None:
|
|
149
|
+
"""清空缓存(测试与画像更新后调用)。"""
|
|
150
|
+
_cache.pop("profile", None)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lang_detect.py — 查询语言检测(统一多语种判定)
|
|
3
|
+
|
|
4
|
+
覆盖全球主要语系:中文/日文/韩文/拉丁语系(英法德西等)/西里尔(俄语等)/
|
|
5
|
+
泰语/阿拉伯/希伯来/希腊/梵文天城体等。纯 stdlib、<1ms。
|
|
6
|
+
|
|
7
|
+
关键决策:
|
|
8
|
+
- 汉字(CJK Unified)归 zh;但日文假名(\\u3040-\\u30ff)与韩文谚文
|
|
9
|
+
(\\uac00-\\ud7af)是强语言信号,优先于汉字判定 —— 「東京の天気」虽是
|
|
10
|
+
汉字+假名,应判 ja 而非 zh。
|
|
11
|
+
- english_ratio 改为「拉丁字母占比」而非「1 - chinese_ratio」,
|
|
12
|
+
修复日韩查询被误判为英文的老问题。
|
|
13
|
+
- script 与 primary_lang 同源(由 detect_language 映射),避免
|
|
14
|
+
「主语言 zh / script latin」这类混技术词查询上的矛盾。
|
|
15
|
+
- 引擎语言参数(setlang/hl/lang/uselang)表只在此维护一处,
|
|
16
|
+
engines_base / local-search/search_v3 共用。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_RE_HAN = re.compile(r"[\u4e00-\u9fff]")
|
|
25
|
+
_RE_KANA = re.compile(r"[\u3040-\u30ff]") # 平假名 + 片假名
|
|
26
|
+
_RE_HANGUL = re.compile(r"[\uac00-\ud7af]") # 谚文音节
|
|
27
|
+
_RE_LATIN = re.compile(r"[A-Za-z]")
|
|
28
|
+
_RE_CYRILLIC = re.compile(r"[\u0400-\u04ff]") # 西里尔(俄/乌/保等)
|
|
29
|
+
_RE_THAI = re.compile(r"[\u0e00-\u0e7f]") # 泰文
|
|
30
|
+
_RE_ARABIC = re.compile(r"[\u0600-\u06ff]") # 阿拉伯
|
|
31
|
+
_RE_HEBREW = re.compile(r"[\u0590-\u05ff]") # 希伯来
|
|
32
|
+
_RE_GREEK = re.compile(r"[\u0370-\u03ff]") # 希腊
|
|
33
|
+
_RE_DEVANAGARI = re.compile(r"[\u0900-\u097f]") # 天城体(印地/梵)
|
|
34
|
+
_RE_LATIN_EXT = re.compile(r"[\u00c0-\u024f]") # 拉丁扩展(é ü ñ 等)
|
|
35
|
+
|
|
36
|
+
LANG_LABELS = {
|
|
37
|
+
"zh": "中文",
|
|
38
|
+
"en": "英文",
|
|
39
|
+
"ja": "日文",
|
|
40
|
+
"ko": "韩文",
|
|
41
|
+
"latin": "拉丁语系",
|
|
42
|
+
"cyrillic": "西里尔语系",
|
|
43
|
+
"thai": "泰语",
|
|
44
|
+
"arabic": "阿拉伯语",
|
|
45
|
+
"hebrew": "希伯来语",
|
|
46
|
+
"greek": "希腊语",
|
|
47
|
+
"devanagari": "印地语系",
|
|
48
|
+
"mixed": "混合",
|
|
49
|
+
"other": "其他",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# primary_lang → 书写系统类别(与 detect_language 同源,杜绝双来源)
|
|
53
|
+
_LANG_TO_SCRIPT: dict[str, str] = {
|
|
54
|
+
"zh": "cjk",
|
|
55
|
+
"ja": "kana",
|
|
56
|
+
"ko": "hangul",
|
|
57
|
+
"en": "latin",
|
|
58
|
+
"latin": "latin",
|
|
59
|
+
"cyrillic": "cyrillic",
|
|
60
|
+
"thai": "thai",
|
|
61
|
+
"arabic": "arabic",
|
|
62
|
+
"hebrew": "hebrew",
|
|
63
|
+
"greek": "greek",
|
|
64
|
+
"devanagari": "devanagari",
|
|
65
|
+
"mixed": "mixed",
|
|
66
|
+
"other": "other",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# 非拉丁主语言:路由/跨语言回退共用
|
|
70
|
+
NON_LATIN_LANGS = frozenset({
|
|
71
|
+
"zh", "ja", "ko", "cyrillic", "thai", "arabic",
|
|
72
|
+
"hebrew", "greek", "devanagari",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# 引擎语言参数 → 各主语言取值(覆盖 config.yaml 静态值)
|
|
76
|
+
# 缺语言键时 engine_lang_param 返回空串,调用方保留静态默认。
|
|
77
|
+
ENGINE_LANG_PARAM_VALUES: dict[str, dict[str, str]] = {
|
|
78
|
+
"setlang": { # Bing
|
|
79
|
+
"ja": "ja-JP", "ko": "ko-KR", "en": "en-US", "latin": "en-US",
|
|
80
|
+
"zh": "zh-Hans", "cyrillic": "ru-RU", "thai": "th-TH",
|
|
81
|
+
"arabic": "ar", "hebrew": "he", "greek": "el", "devanagari": "hi",
|
|
82
|
+
"mixed": "en-US", "other": "en-US",
|
|
83
|
+
},
|
|
84
|
+
"mkt": { # Bing 市场码(比 setlang 更强制结果区域,修复 ja/ko 返回中文站问题)
|
|
85
|
+
"ja": "ja-JP", "ko": "ko-KR", "en": "en-US", "latin": "en-US",
|
|
86
|
+
"zh": "zh-CN", "cyrillic": "ru-RU", "thai": "th-TH",
|
|
87
|
+
"arabic": "ar-SA", "hebrew": "he-IL", "greek": "el-GR", "devanagari": "hi-IN",
|
|
88
|
+
"mixed": "en-US", "other": "en-US",
|
|
89
|
+
},
|
|
90
|
+
"hl": { # Google
|
|
91
|
+
"ja": "ja", "ko": "ko", "en": "en", "latin": "en",
|
|
92
|
+
"zh": "zh-CN", "cyrillic": "ru", "thai": "th",
|
|
93
|
+
"arabic": "ar", "hebrew": "iw", "greek": "el", "devanagari": "hi",
|
|
94
|
+
"mixed": "en", "other": "en",
|
|
95
|
+
},
|
|
96
|
+
"lang": { # Yandex
|
|
97
|
+
"ja": "ja", "ko": "ko", "en": "en", "latin": "en", "zh": "zh",
|
|
98
|
+
"cyrillic": "ru", "thai": "th",
|
|
99
|
+
"arabic": "ar", "hebrew": "he", "greek": "el", "devanagari": "hi",
|
|
100
|
+
"mixed": "en", "other": "en",
|
|
101
|
+
},
|
|
102
|
+
"uselang": { # Wikipedia
|
|
103
|
+
"ja": "ja", "ko": "ko", "en": "en", "latin": "en", "zh": "zh",
|
|
104
|
+
"cyrillic": "ru", "thai": "th",
|
|
105
|
+
"arabic": "ar", "hebrew": "he", "greek": "el", "devanagari": "hi",
|
|
106
|
+
"mixed": "en", "other": "en",
|
|
107
|
+
},
|
|
108
|
+
"region": { # ddgs -r region({country}-{lang} 形态)
|
|
109
|
+
"ja": "jp-ja", "ko": "kr-ko", "en": "us-en", "latin": "us-en",
|
|
110
|
+
"zh": "cn-zh", "cyrillic": "ru-ru", "thai": "th-th",
|
|
111
|
+
"arabic": "sa-ar", "hebrew": "il-he", "greek": "gr-el", "devanagari": "in-hi",
|
|
112
|
+
"mixed": "us-en", "other": "us-en",
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_LANG_PARAM_KEYS = frozenset(ENGINE_LANG_PARAM_VALUES.keys())
|
|
117
|
+
|
|
118
|
+
# 纯汉字日文特征词:简体/繁体中文查询中均不出现(或极罕见)的日语词汇。
|
|
119
|
+
# 用于区分「无假名日文查询」与「中文查询」,避免被 han/total 分支误判为 zh。
|
|
120
|
+
_JA_KANJI_ONLY_SIGNALS = (
|
|
121
|
+
"非同期", "処理", "予報", "検索", "天気", "株式", "映画", "配信",
|
|
122
|
+
"報道", "速報", "転職", "面接", "給料", "応募", "卒業", "受験",
|
|
123
|
+
"大学院", "授業", "講義", "宿題", "就活", "新卒", "口座", "振込",
|
|
124
|
+
"残高", "通帳", "介護", "処方", "発熱", "予定", "営業", "退職",
|
|
125
|
+
"支払", "申告", "円高", "円安", "株価", "参議院", "衆議院", "住民票",
|
|
126
|
+
"印鑑", "免許", "交番", "弁護士", "捜査", "容疑者", "津波", "断水",
|
|
127
|
+
"変電", "給湯", "残業", "有給", "退院", "受診", "処方箋", "薬局",
|
|
128
|
+
"予約", "出張", "帰省", "新幹線", "改札", "切符", "乗車", "納期",
|
|
129
|
+
"発注", "受注", "出荷", "納品", "売上", "仕入", "原価", "在庫",
|
|
130
|
+
"領収書", "請求書", "見積",
|
|
131
|
+
# AI / 技术高频(实测缺口:查询「人工知能 最新 動向」曾因不在表中被判成 zh)
|
|
132
|
+
"人工知能", "機械学習", "深層学習", "情報処理", "開発環境", "実行",
|
|
133
|
+
"変数", "関数", "引数", "配列", "例外", "継承", "抽象", "再帰",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 日文新字体中「既不同于简体、也不同于繁体」的独有字形。
|
|
137
|
+
#
|
|
138
|
+
# 判定依据(逐个核对字形三态):
|
|
139
|
+
# 観(ja) / 观(zh简) / 觀(zh繁) → 仅日文用「観」
|
|
140
|
+
# 経(ja) / 经(zh简) / 經(zh繁) → 仅日文用「経」
|
|
141
|
+
# 鉄(ja) / 铁(zh简) / 鐵(zh繁) → 仅日文用「鉄」
|
|
142
|
+
# 这类字形出现即**可以确定**是日文,不会误伤简体或繁体中文。
|
|
143
|
+
#
|
|
144
|
+
# 反面教训(为什么不用「動/報/學」这类字形):它们是日文新字体,
|
|
145
|
+
# 但**同时是繁体中文用字**(繁中亦作「動」「報」「學」),拿来判日文
|
|
146
|
+
# 会把繁体中文误判成日文。故只用「三态互异」的严格子集。
|
|
147
|
+
#
|
|
148
|
+
# 已知局限(本函数无法覆盖):纯粹由中日共用汉字组成的短查询,
|
|
149
|
+
# 如「人工知能 最新 動向」——它不含任何日文独有字形,字符层面与中文
|
|
150
|
+
# 无从区分,只能靠上面的词汇表命中(该例已通过补词解决)。若两者
|
|
151
|
+
# 都不命中,会落到 zh 分支。这是一个**已知且有界的**限制,不做硬猜。
|
|
152
|
+
_JA_SHINJITAI_ONLY = frozenset(
|
|
153
|
+
"亀亜伝価児円処剣剤単厳営団囲図圧塩壊売変奨実寛対専帯帰庁広弾徴応悪戦戸択拠挙挟摂斉斎桟検楽権歓歯気沢浄済渋滝犠獣産畳発糸経継続総縁縄縦繊聡聴臓芸荘薬蛍覚観証読譲賛転軽辺逓遅郷醸鉄鉱鋳関闘険雑顕駅騒験"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _has_ja_only_kanji(text: str) -> bool:
|
|
158
|
+
"""文本是否含「仅日文使用」的新字体字形(三态互异)。"""
|
|
159
|
+
return any(c in _JA_SHINJITAI_ONLY for c in text)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def detect_language(query: str) -> str:
|
|
163
|
+
"""返回主语言标签:zh / en / ja / ko / latin / cyrillic / thai / arabic /
|
|
164
|
+
hebrew / greek / devanagari / mixed / other。"""
|
|
165
|
+
if not query or not query.strip():
|
|
166
|
+
return "other"
|
|
167
|
+
total_chars = len([c for c in query if not c.isspace()])
|
|
168
|
+
if total_chars == 0:
|
|
169
|
+
return "other"
|
|
170
|
+
han = len(_RE_HAN.findall(query))
|
|
171
|
+
kana = len(_RE_KANA.findall(query))
|
|
172
|
+
hangul = len(_RE_HANGUL.findall(query))
|
|
173
|
+
latin = len(_RE_LATIN.findall(query))
|
|
174
|
+
latin_ext = len(_RE_LATIN_EXT.findall(query))
|
|
175
|
+
cyrillic = len(_RE_CYRILLIC.findall(query))
|
|
176
|
+
thai = len(_RE_THAI.findall(query))
|
|
177
|
+
arabic = len(_RE_ARABIC.findall(query))
|
|
178
|
+
hebrew = len(_RE_HEBREW.findall(query))
|
|
179
|
+
greek = len(_RE_GREEK.findall(query))
|
|
180
|
+
devanagari = len(_RE_DEVANAGARI.findall(query))
|
|
181
|
+
|
|
182
|
+
# 非拉丁书写系统强信号优先
|
|
183
|
+
if kana / total_chars >= 0.15:
|
|
184
|
+
return "ja"
|
|
185
|
+
if hangul / total_chars >= 0.15:
|
|
186
|
+
return "ko"
|
|
187
|
+
if kana and han and han / total_chars >= 0.3:
|
|
188
|
+
return "ja" # 汉字 + 少量假名(如「東京の天気」)
|
|
189
|
+
if thai / total_chars >= 0.2:
|
|
190
|
+
return "thai"
|
|
191
|
+
if arabic / total_chars >= 0.2:
|
|
192
|
+
return "arabic"
|
|
193
|
+
if hebrew / total_chars >= 0.2:
|
|
194
|
+
return "hebrew"
|
|
195
|
+
if greek / total_chars >= 0.2:
|
|
196
|
+
return "greek"
|
|
197
|
+
if devanagari / total_chars >= 0.2:
|
|
198
|
+
return "devanagari"
|
|
199
|
+
if cyrillic / total_chars >= 0.2:
|
|
200
|
+
return "cyrillic"
|
|
201
|
+
# 纯汉字日文预检(两道,互为补充):
|
|
202
|
+
# ① 日文独有字形(三态互异,如 観/経/鉄)——出现即可确定是日文,
|
|
203
|
+
# 不会误伤简体或繁体中文,可靠性最高。
|
|
204
|
+
# ② 日语特征词表——覆盖不含独有字形的常见日文表达。
|
|
205
|
+
#
|
|
206
|
+
# 已知且**有界**的残余局限:纯粹由中日共用汉字组成的短查询
|
|
207
|
+
# (如「人工知能 最新 動向」——「動」繁体中文亦用,不能作判据)
|
|
208
|
+
# 在字符层面与中文无从区分。此类查询若两道路径都不命中,会落到 zh。
|
|
209
|
+
# 不做硬猜:宁可保持现状(由调用方的语言覆盖/多语言回退保底),
|
|
210
|
+
# 也不引入会把简体中文误判成日文的正则。
|
|
211
|
+
if han and (_has_ja_only_kanji(query)
|
|
212
|
+
or any(w in query for w in _JA_KANJI_ONLY_SIGNALS)):
|
|
213
|
+
return "ja"
|
|
214
|
+
# 汉字为主判 zh
|
|
215
|
+
if han / total_chars >= 0.3:
|
|
216
|
+
return "zh"
|
|
217
|
+
# 拉丁字母(含扩展字符 é/ü/ñ 等)为主 → 拉丁语系
|
|
218
|
+
# 扩展字符占比 ≥ 0.08 时判 latin(café / español / français 等)
|
|
219
|
+
if (latin + latin_ext) / total_chars >= 0.5:
|
|
220
|
+
if latin_ext / total_chars >= 0.08:
|
|
221
|
+
return "latin"
|
|
222
|
+
return "en" if latin else "latin"
|
|
223
|
+
# 有汉字也有大量拉丁 → 中英混合;其余情况判混合
|
|
224
|
+
if han and latin:
|
|
225
|
+
return "mixed"
|
|
226
|
+
return "other"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def detect_script(query: str) -> str:
|
|
230
|
+
"""返回书写系统类别,与 detect_language 同源映射。
|
|
231
|
+
|
|
232
|
+
不再独立计分:避免「Python 异步编程」主语言 zh 却 script=latin 的矛盾。
|
|
233
|
+
供路由做 is_latin / 跨语言回退判断。
|
|
234
|
+
"""
|
|
235
|
+
return _LANG_TO_SCRIPT.get(detect_language(query), "other")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def is_non_latin_lang(lang: str) -> bool:
|
|
239
|
+
"""主语言是否为非拉丁书写系统(需要跨语言回退/语言引擎补充)。"""
|
|
240
|
+
return lang in NON_LATIN_LANGS
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def engine_lang_param(param: str, query: str, preferred_lang: str = "") -> str:
|
|
244
|
+
"""按查询主语言返回引擎语言参数取值;无法判定或无映射时返回空串。
|
|
245
|
+
|
|
246
|
+
调用方约定:`v = engine_lang_param(k, query) or v`,保留 config 静态默认。
|
|
247
|
+
|
|
248
|
+
preferred_lang:弱信号查询(mixed/other)时的回落语种,来自
|
|
249
|
+
lang_pref.effective_engine_lang(习惯/系统/中英基线)。强查询信号始终优先。
|
|
250
|
+
"""
|
|
251
|
+
table = ENGINE_LANG_PARAM_VALUES.get(param)
|
|
252
|
+
if not table:
|
|
253
|
+
return ""
|
|
254
|
+
lang = detect_language(query)
|
|
255
|
+
# 弱信号才采用偏好语;zh/ja/ko/非拉丁/en/latin 以查询为准
|
|
256
|
+
if preferred_lang and lang in ("mixed", "other"):
|
|
257
|
+
if preferred_lang in table:
|
|
258
|
+
return table[preferred_lang]
|
|
259
|
+
return table.get(lang, "")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def is_lang_param(key: str) -> bool:
|
|
263
|
+
"""extra_params 键是否为可动态覆盖的语言参数。"""
|
|
264
|
+
return key in _LANG_PARAM_KEYS
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def language_features(query: str) -> dict[str, Any]:
|
|
268
|
+
"""返回与 route.extract_features 兼容的语言特征字段。
|
|
269
|
+
|
|
270
|
+
新增 primary_lang / script / is_latin,并保留 chinese_ratio /
|
|
271
|
+
english_ratio 的既有语义(chinese_ratio 仍为汉字占比,english_ratio
|
|
272
|
+
为拉丁占比),兼容现有调用方(route / smart_router 的 is_chinese
|
|
273
|
+
判断不破坏)。
|
|
274
|
+
"""
|
|
275
|
+
if not query:
|
|
276
|
+
query = ""
|
|
277
|
+
total = max(len(query), 1)
|
|
278
|
+
chinese = len(_RE_HAN.findall(query))
|
|
279
|
+
latin = len(_RE_LATIN.findall(query)) + len(_RE_LATIN_EXT.findall(query))
|
|
280
|
+
primary_lang = detect_language(query)
|
|
281
|
+
script = _LANG_TO_SCRIPT.get(primary_lang, "other")
|
|
282
|
+
return {
|
|
283
|
+
"primary_lang": primary_lang,
|
|
284
|
+
"script": script,
|
|
285
|
+
"is_latin": primary_lang in ("en", "latin"),
|
|
286
|
+
"chinese_ratio": round(chinese / total, 4),
|
|
287
|
+
"english_ratio": round(latin / total, 4),
|
|
288
|
+
"length": total,
|
|
289
|
+
}
|