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,775 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""专用构建器:宏观数据引擎(FRED / 汇率 / 世界银行 / 国家统计局 / Eurostat)
|
|
3
|
+
|
|
4
|
+
自 engines_builders_data.py 拆分,减少单文件体积;数据表随各自 builder 集中。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from engines_base import (
|
|
20
|
+
safe_search, _run, _resolve, _get_path, _coerce_field, _detect_anti_bot,
|
|
21
|
+
http_open, rank_score,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("unified_search.engines")
|
|
25
|
+
|
|
26
|
+
# ── FRED 宏观数据引擎 ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
# 常用宏观指标 → FRED series_id(关键词前缀匹配,命中即拉取)
|
|
29
|
+
_FRED_SERIES: dict[str, str] = {
|
|
30
|
+
# 物价
|
|
31
|
+
"cpi": "CPIAUCSL", "通胀": "CPIAUCSL", "消费者物价": "CPIAUCSL", "物价指数": "CPIAUCSL",
|
|
32
|
+
"核心cpi": "CPILFESL", "ppi": "PPIACO", "生产者价格": "PPIACO", "pce": "PCEPI",
|
|
33
|
+
"个人消费支出": "PCEPI",
|
|
34
|
+
# 就业
|
|
35
|
+
"失业率": "UNRATE", "非农": "PAYEMS", "就业": "PAYEMS", "初请": "ICSA", "申请失业金": "ICSA",
|
|
36
|
+
# 利率
|
|
37
|
+
"联邦基金": "FEDFUNDS", "联邦基金利率": "FEDFUNDS", "美联储利率": "FEDFUNDS",
|
|
38
|
+
"十年期": "DGS10", "10年期": "DGS10", "国债收益率": "DGS10", "美债收益率": "DGS10",
|
|
39
|
+
"三十年期": "DGS30", "30年期": "DGS30", "两年期": "DGS2", "2年期": "DGS2",
|
|
40
|
+
"三个月国债": "TB3MS", "3个月国债": "TB3MS", "国库券": "TB3MS",
|
|
41
|
+
"sofr": "SOFR", "隔夜回购": "SOFR",
|
|
42
|
+
# 增长 / 货币
|
|
43
|
+
"gdp": "GDP", "国内生产总值": "GDP",
|
|
44
|
+
"m2": "M2SL", "货币供应": "M2SL", "m1": "M1SL",
|
|
45
|
+
"零售": "RSAFS", "零售销售": "RSAFS",
|
|
46
|
+
"美元指数": "DTWEXBGS", "贸易加权美元": "DTWEXBGS",
|
|
47
|
+
}
|
|
48
|
+
_FRED_SERIES_FALLBACK = ["CPIAUCSL", "UNRATE", "FEDFUNDS", "DGS10", "GDP", "M2SL"]
|
|
49
|
+
|
|
50
|
+
# series_id → 展示标签(标题用中文指标名)
|
|
51
|
+
_FRED_LABELS: dict[str, str] = {
|
|
52
|
+
"CPIAUCSL": "美国CPI(消费者物价指数)",
|
|
53
|
+
"CPILFESL": "美国核心CPI",
|
|
54
|
+
"PPIACO": "美国PPI(生产者价格指数)",
|
|
55
|
+
"PCEPI": "美国PCE(个人消费支出物价)",
|
|
56
|
+
"UNRATE": "美国失业率",
|
|
57
|
+
"PAYEMS": "美国非农就业",
|
|
58
|
+
"ICSA": "美国初请失业金",
|
|
59
|
+
"FEDFUNDS": "美国联邦基金利率",
|
|
60
|
+
"DGS10": "美国10年期国债收益率",
|
|
61
|
+
"DGS30": "美国30年期国债收益率",
|
|
62
|
+
"DGS2": "美国2年期国债收益率",
|
|
63
|
+
"TB3MS": "美国3个月国库券收益率",
|
|
64
|
+
"SOFR": "SOFR(隔夜融资利率)",
|
|
65
|
+
"GDP": "美国GDP(国内生产总值)",
|
|
66
|
+
"M2SL": "美国M2货币供应",
|
|
67
|
+
"M1SL": "美国M1货币供应",
|
|
68
|
+
"RSAFS": "美国零售销售",
|
|
69
|
+
"DTWEXBGS": "美元指数(贸易加权)",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_fred_engine(spec: dict[str, Any]) -> Any:
|
|
74
|
+
"""FRED 宏观数据(fredgraph.csv 免认证)。
|
|
75
|
+
|
|
76
|
+
按关键词映射 series_id,拉取近 N 个观测值,展示最新值 + 趋势方向。
|
|
77
|
+
"""
|
|
78
|
+
timeout = spec.get("timeout", 10)
|
|
79
|
+
|
|
80
|
+
@safe_search
|
|
81
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
82
|
+
to = _timeout or timeout
|
|
83
|
+
# 国家词守卫:FRED 序列均为美国或全球计算方式,明确指向其他国家时放弃,
|
|
84
|
+
# 避免「中国GDP」被美国序列冒充(由 worldbank 按国家参数接管)
|
|
85
|
+
if is_foreign_macro_query(query):
|
|
86
|
+
return []
|
|
87
|
+
q = query.lower()
|
|
88
|
+
series_id = None
|
|
89
|
+
for kw, sid in _FRED_SERIES.items():
|
|
90
|
+
if kw in q:
|
|
91
|
+
series_id = sid
|
|
92
|
+
break
|
|
93
|
+
if not series_id:
|
|
94
|
+
return []
|
|
95
|
+
url = f"https://fred.stlouisfed.org/graph/fredgraph.csv?id={series_id}"
|
|
96
|
+
try:
|
|
97
|
+
req = urllib.request.Request(url, headers={"User-Agent": "argo-search/2.4 (unified-search@local)"})
|
|
98
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
99
|
+
text = resp.read().decode("utf-8", "replace")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(f"FRED 拉取失败 ({series_id}): {e}")
|
|
102
|
+
return []
|
|
103
|
+
rows = []
|
|
104
|
+
for line in text.strip().splitlines()[1:]:
|
|
105
|
+
if "," not in line:
|
|
106
|
+
continue
|
|
107
|
+
date, _, val = line.partition(",")
|
|
108
|
+
val = val.strip()
|
|
109
|
+
if val in ("", "."):
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
rows.append((date, float(val)))
|
|
113
|
+
except ValueError:
|
|
114
|
+
continue
|
|
115
|
+
if not rows:
|
|
116
|
+
return []
|
|
117
|
+
# 近 N 期各为一条结果(url 带日期锚点防去重合并),RRF 分数累计上浮;
|
|
118
|
+
# 反转使最新一期 rank 0(首条=最新值+趋势方向),旧写法最旧一条排首
|
|
119
|
+
label = _FRED_LABELS.get(series_id, f"FRED {series_id}")
|
|
120
|
+
results = []
|
|
121
|
+
for _rk, (date, val) in enumerate(reversed(rows[-min(n, 5):])):
|
|
122
|
+
results.append({
|
|
123
|
+
"title": f"{label} · {date} = {val:g}",
|
|
124
|
+
"url": f"https://fred.stlouisfed.org/series/{series_id}?obs={date}",
|
|
125
|
+
"snippet": f"FRED {series_id} 最新值 {val:g}(截至 {date})",
|
|
126
|
+
"source": "fred",
|
|
127
|
+
"score": rank_score(0.9, _rk),
|
|
128
|
+
})
|
|
129
|
+
# 首条附趋势方向
|
|
130
|
+
if len(results) > 1:
|
|
131
|
+
prev = rows[-2]
|
|
132
|
+
diff = rows[-1][1] - prev[1]
|
|
133
|
+
arrow = "↑" if diff > 0 else ("↓" if diff < 0 else "→")
|
|
134
|
+
results[0]["snippet"] = (f"FRED {series_id} 最新值 {rows[-1][1]:g}({rows[-1][0]})"
|
|
135
|
+
f" {arrow}{abs(diff):.2f}")
|
|
136
|
+
return results
|
|
137
|
+
return _engine
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── 汇率引擎(open.er-api.com 免认证) ───────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
_CURRENCY_ALIASES: dict[str, str] = {
|
|
144
|
+
"人民币": "CNY", "在岸人民币": "CNY", "离岸人民币": "CNH",
|
|
145
|
+
"美元": "USD", "美金": "USD", "欧元": "EUR", "英镑": "GBP", "日元": "JPY",
|
|
146
|
+
"港币": "HKD", "港元": "HKD", "澳门元": "MOP", "澳元": "AUD",
|
|
147
|
+
"加元": "CAD", "瑞郎": "CHF", "新西兰元": "NZD",
|
|
148
|
+
"新加坡元": "SGD", "韩元": "KRW", "卢布": "RUB", "印度卢比": "INR",
|
|
149
|
+
"泰铢": "THB", "林吉特": "MYR", "印尼盾": "IDR", "越南盾": "VND",
|
|
150
|
+
"台币": "TWD", "新台币": "TWD", "比索": "PHP", "里拉": "TRY",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# 合法 ISO 4217 代码集合(别名表 + 常见直报代码)。查询里的任意三字母大写
|
|
154
|
+
# 缩写(GDP/CPI/ETF/IPO/AI…)不能被当成货币代码——此前 `USD GDP 汇率` 会把
|
|
155
|
+
# GDP 当 target,接口必然 404;`CPI 汇率` 更糟,两个缩写凑出一对假货币。
|
|
156
|
+
_ISO_CODES: frozenset[str] = frozenset(_CURRENCY_ALIASES.values()) | frozenset({
|
|
157
|
+
"AED", "ARS", "BRL", "CLP", "COP", "CZK", "DKK", "EGP", "ILS", "ISK",
|
|
158
|
+
"KWD", "MXN", "NOK", "PKR", "PLN", "QAR", "RON", "SAR", "SEK", "SGD",
|
|
159
|
+
"THB", "TWD", "UAH", "VND", "ZAR", "CNH", "MOP", "PHP", "TRY",
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_fx_pair(q: str) -> tuple[str, str]:
|
|
164
|
+
"""从查询文本解析 (base, target) 货币对;解析不出返回 ("", "")。
|
|
165
|
+
|
|
166
|
+
规则(按优先级):
|
|
167
|
+
0. 「X兑Y / X对Y / X/Y / X比Y」显式方向——出现在查询里即以它为准;
|
|
168
|
+
1. 两个合法 ISO 代码;
|
|
169
|
+
2. 一个代码 + 一个中文别名;
|
|
170
|
+
3. 两个中文别名;
|
|
171
|
+
4. 单个中文别名 → 该货币对 CNY(人民币自身 → USD/CNY 市场惯例报价)。
|
|
172
|
+
|
|
173
|
+
报价方向采市场惯例:中文财经语境里「人民币兑美元」的牌价数字是
|
|
174
|
+
1 美元 = 6.7x 人民币(USD/CNY),「日元兑美元跌到 150」是 USD/JPY。
|
|
175
|
+
EUR/GBP/AUD/NZD 是豁免组(它们对 USD 的惯例报价是 X/USD)。引擎本身
|
|
176
|
+
同时返回反向汇率一行,交换只影响标题口径,不丢信息。
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def _match_currency(token: str) -> str:
|
|
180
|
+
"""精确匹配别名或代码;失败时前缀匹配(人民币汇率→人民币)。"""
|
|
181
|
+
token = token.strip()
|
|
182
|
+
if not token:
|
|
183
|
+
return ""
|
|
184
|
+
if token in _CURRENCY_ALIASES:
|
|
185
|
+
return _CURRENCY_ALIASES[token]
|
|
186
|
+
if re.fullmatch(r"[A-Z]{3}", token.upper()) and token.upper() in _ISO_CODES:
|
|
187
|
+
return token.upper()
|
|
188
|
+
# 前缀匹配:token 以某别名开头,或某别名以 token 开头。
|
|
189
|
+
# 长别名优先(「离岸人民币」优先于「人民币」被「人民币汇率」误命中)。
|
|
190
|
+
for name in sorted(_CURRENCY_ALIASES, key=len, reverse=True):
|
|
191
|
+
if token.startswith(name) or name.startswith(token):
|
|
192
|
+
return _CURRENCY_ALIASES[name]
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
def _norm(s: str) -> str:
|
|
196
|
+
return s.strip(" 的是多少报查一下今天今日现在最新价位汇率中间价卖出价买入价行情走势")
|
|
197
|
+
|
|
198
|
+
codes = [c for c in re.findall(r"\b([A-Z]{3})\b", q.upper()) if c in _ISO_CODES]
|
|
199
|
+
aliases = [c for c in sorted(_CURRENCY_ALIASES, key=len, reverse=True) if c in q]
|
|
200
|
+
base, target = "", ""
|
|
201
|
+
if len(codes) >= 2:
|
|
202
|
+
base, target = codes[0], codes[1]
|
|
203
|
+
elif len(codes) == 1 and aliases:
|
|
204
|
+
if codes[0] in ("CNY", "USD", "EUR"):
|
|
205
|
+
base, target = codes[0], _CURRENCY_ALIASES[aliases[0]]
|
|
206
|
+
else:
|
|
207
|
+
base, target = "CNY", codes[0]
|
|
208
|
+
elif len(aliases) >= 2:
|
|
209
|
+
base, target = _CURRENCY_ALIASES[aliases[0]], _CURRENCY_ALIASES[aliases[1]]
|
|
210
|
+
elif len(aliases) == 1:
|
|
211
|
+
# 单别名单货币:非人民币货币 → 它对 CNY(「美元汇率」= 1 美元 = x 人民币,
|
|
212
|
+
# 与央行中间价同口径);人民币自身 → USD/CNY(「人民币汇率」问的是
|
|
213
|
+
# 1 美元兑多少人民币,这是唯一有市场行情的报价方向)。
|
|
214
|
+
code = _CURRENCY_ALIASES[aliases[0]]
|
|
215
|
+
base, target = ("USD", "CNY") if code == "CNY" else (code, "CNY")
|
|
216
|
+
|
|
217
|
+
# 显式方向词:「X兑Y」「X对Y」「X/Y」「X比Y」——出现在查询里即以它为准。
|
|
218
|
+
# split 取首末两段分别解析,中间的噪音词(「今日」「中间价」)不参与。
|
|
219
|
+
sep = re.search(r"[//]|兑|対|对|比", q)
|
|
220
|
+
if sep:
|
|
221
|
+
left, right = q[:sep.start()], q[sep.end():]
|
|
222
|
+
lc = _match_currency(_norm(left)) or _match_currency(left)
|
|
223
|
+
# 右侧只取尾段前 6 字(「美元 中间价 今日」里兑后还有牌价词)
|
|
224
|
+
rc = _match_currency(_norm(right)) or _match_currency(right[:6])
|
|
225
|
+
if lc and rc:
|
|
226
|
+
if lc == rc:
|
|
227
|
+
return "", "" # 显式同货币对(「人民币兑人民币」)= 无效查询
|
|
228
|
+
base, target = lc, rc
|
|
229
|
+
|
|
230
|
+
# CNY/JPY 等货币对 USD 的报价方向采市场惯例:EUR/GBP/AUD/NZD 之外,
|
|
231
|
+
# 中文财经语境里「日元兑美元跌到 150」的数字是 USD/JPY,不是 JPY/USD;
|
|
232
|
+
# 「人民币兑美元报 6.72」同理是 USD/CNY。引擎同时返回反向汇率一行,
|
|
233
|
+
# 交换只影响标题口径,不丢信息。
|
|
234
|
+
_USD_BASE_EXEMPT = frozenset({"EUR", "GBP", "AUD", "NZD"})
|
|
235
|
+
if base and target and base != target:
|
|
236
|
+
if base == "CNY":
|
|
237
|
+
base, target = target, base
|
|
238
|
+
elif target == "USD" and base not in _USD_BASE_EXEMPT:
|
|
239
|
+
base, target = "USD", base
|
|
240
|
+
elif base == "USD" and target in _USD_BASE_EXEMPT:
|
|
241
|
+
base, target = target, "USD"
|
|
242
|
+
if base == target:
|
|
243
|
+
base, target = "", "" # 退化对(CNY→CNY)不出网,交由其它源回答
|
|
244
|
+
return base, target
|
|
245
|
+
|
|
246
|
+
def _build_fx_rate_engine(spec: dict[str, Any]) -> Any:
|
|
247
|
+
"""实时汇率(open.er-api.com/v6/latest,免认证)。
|
|
248
|
+
|
|
249
|
+
命中"美元兑人民币"类查询:解析两种货币代码,返回最新汇率快照。
|
|
250
|
+
"""
|
|
251
|
+
timeout = spec.get("timeout", 10)
|
|
252
|
+
api_url = "https://open.er-api.com/v6/latest/"
|
|
253
|
+
|
|
254
|
+
@safe_search
|
|
255
|
+
def _engine(query: str, n: int = 3, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
256
|
+
to = _timeout or timeout
|
|
257
|
+
base, target = parse_fx_pair(query)
|
|
258
|
+
if not base or not target:
|
|
259
|
+
return []
|
|
260
|
+
try:
|
|
261
|
+
req = urllib.request.Request(api_url + base, headers={"User-Agent": "argo-search/2.4 (unified-search@local)"})
|
|
262
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
263
|
+
data = json.loads(resp.read().decode("utf-8", "replace"))
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(f"汇率接口失败 ({base}): {e}")
|
|
266
|
+
return []
|
|
267
|
+
if data.get("result") != "success":
|
|
268
|
+
return []
|
|
269
|
+
rates = data.get("rates") or {}
|
|
270
|
+
if target not in rates:
|
|
271
|
+
return []
|
|
272
|
+
rate = rates[target]
|
|
273
|
+
date = data.get("time_last_update_utc") or data.get("time_last_update_unix") or ""
|
|
274
|
+
results = [{
|
|
275
|
+
"title": f"1 {base} = {rate:g} {target}",
|
|
276
|
+
"url": f"https://www.exchangerate-api.com/{base.lower()}-{target.lower()}",
|
|
277
|
+
"snippet": f"实时汇率 {base}/{target} = {rate:g}({date},open.er-api.com)",
|
|
278
|
+
"source": "fx_rate",
|
|
279
|
+
"score": 0.9,
|
|
280
|
+
}]
|
|
281
|
+
# 反向汇率补充一条
|
|
282
|
+
if rate:
|
|
283
|
+
results.append({
|
|
284
|
+
"title": f"1 {target} = {1 / rate:.4g} {base}",
|
|
285
|
+
"url": f"https://www.exchangerate-api.com/{target.lower()}-{base.lower()}",
|
|
286
|
+
"snippet": f"反向汇率 {target}/{base} = {1 / rate:.4g}(open.er-api.com)",
|
|
287
|
+
"source": "fx_rate",
|
|
288
|
+
"score": 0.8,
|
|
289
|
+
})
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
return _engine
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ── 世界银行开放数据引擎(宏观指标,免认证) ─────────────────────────────────
|
|
297
|
+
|
|
298
|
+
# 国家表与两个纯文本谓词已拆到 macro_countries:它们被路由层直接引用,而路由
|
|
299
|
+
# 是每次调用的必经路径;留在本模块会让 route 被迫 import 整条 HTTP 栈(实测
|
|
300
|
+
# 36 ms)。这里只转出名字,不改语义。
|
|
301
|
+
from macro_countries import ( # noqa: E402
|
|
302
|
+
WORLDBANK_COUNTRIES,
|
|
303
|
+
is_foreign_macro_query,
|
|
304
|
+
match_country as _match_country,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _build_worldbank_engine(spec: dict[str, Any]) -> Any:
|
|
309
|
+
"""世界银行开放数据(api.worldbank.org,免认证,300+ 指标)。
|
|
310
|
+
|
|
311
|
+
覆盖 GDP/人均GDP/通胀/失业/贸易/人口/利率等国家宏观指标,
|
|
312
|
+
macro_data 域与 FRED 互补:FRED 偏美国,worldbank 覆盖全球。
|
|
313
|
+
"""
|
|
314
|
+
timeout = spec.get("timeout", 12)
|
|
315
|
+
# 默认按时间倒序返回最新数据,per_page 直取最近年份;mrnev 参数实测易超时
|
|
316
|
+
api = "https://api.worldbank.org/v2/country/{cc}/indicator/{ind}?format=json&per_page=6"
|
|
317
|
+
|
|
318
|
+
_INDICATORS: list[tuple[tuple[str, ...], str, str]] = [
|
|
319
|
+
(("gdp", "国内生产总值", "生产总值", "经济总量"), "NY.GDP.MKTP.CD", "GDP(现价美元)"),
|
|
320
|
+
(("人均gdp", "人均国内生产总值", "gdp per capita"), "NY.GDP.PCAP.CD", "人均GDP(现价美元)"),
|
|
321
|
+
(("gdp增速", "gdp增长", "经济增长率", "gdp growth"), "NY.GDP.MKTP.KD.ZG", "GDP年增长率"),
|
|
322
|
+
(("cpi", "通胀", "通货膨胀", "物价", "inflation"), "FP.CPI.TOTL.ZG", "CPI通胀率"),
|
|
323
|
+
(("失业", "失业率", "unemployment"), "SL.UEM.TOTL.ZS", "失业率"),
|
|
324
|
+
(("人口", "population"), "SP.POP.TOTL", "总人口"),
|
|
325
|
+
(("贸易", "进出口", "对外贸易", "trade"), "NE.TRD.GNFS.ZS", "贸易占GDP比重"),
|
|
326
|
+
(("利率", "贷款利率", "interest rate"), "FR.INR.LEND", "贷款利率"),
|
|
327
|
+
(("经常账户", "经常项目", "current account"), "BN.CAB.XOKA.GD.ZS", "经常账户占GDP比重"),
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
@safe_search
|
|
331
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
332
|
+
import urllib.parse as up
|
|
333
|
+
to = _timeout or timeout
|
|
334
|
+
headers = {"User-Agent": "argo-search/2.4 (unified-search@local)"}
|
|
335
|
+
cc, ind, ind_label = _parse(query)
|
|
336
|
+
if not cc or not ind:
|
|
337
|
+
return []
|
|
338
|
+
data = None
|
|
339
|
+
for _a in range(2):
|
|
340
|
+
try:
|
|
341
|
+
req = urllib.request.Request(api.format(cc=cc, ind=ind), headers=headers)
|
|
342
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
343
|
+
data = json.loads(resp.read().decode("utf-8", "replace"))
|
|
344
|
+
break
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.warning(f"世界银行数据失败(重试): {e}")
|
|
347
|
+
time.sleep(0.3)
|
|
348
|
+
if data is None:
|
|
349
|
+
return []
|
|
350
|
+
if not isinstance(data, list) or len(data) < 2:
|
|
351
|
+
return []
|
|
352
|
+
rows = data[1]
|
|
353
|
+
if not rows:
|
|
354
|
+
return []
|
|
355
|
+
cname = rows[0].get("country", {}).get("value", cc)
|
|
356
|
+
results = []
|
|
357
|
+
for _rk1, it in enumerate(rows):
|
|
358
|
+
year = it.get("date", "")
|
|
359
|
+
val = it.get("value")
|
|
360
|
+
if val is None or year is None:
|
|
361
|
+
continue
|
|
362
|
+
try:
|
|
363
|
+
f = float(val)
|
|
364
|
+
except (TypeError, ValueError):
|
|
365
|
+
continue
|
|
366
|
+
title = _fmt_title(cname, ind_label, year, f, ind)
|
|
367
|
+
results.append({
|
|
368
|
+
"title": title,
|
|
369
|
+
"url": f"https://data.worldbank.org/indicator/{ind}?locations={cc}",
|
|
370
|
+
"snippet": f"{ind_label} | {cname} | 世界银行开放数据 | 数据截至 {year}".strip(),
|
|
371
|
+
"source": "worldbank",
|
|
372
|
+
"score": rank_score(0.9, _rk1),
|
|
373
|
+
})
|
|
374
|
+
if len(results) >= min(n, 5):
|
|
375
|
+
break
|
|
376
|
+
return results
|
|
377
|
+
|
|
378
|
+
def _parse(q: str) -> tuple[str, str, str]:
|
|
379
|
+
low = q.lower()
|
|
380
|
+
cc = _match_country(q, WORLDBANK_COUNTRIES)
|
|
381
|
+
if not cc:
|
|
382
|
+
return "", "", ""
|
|
383
|
+
ind, ind_label = "", ""
|
|
384
|
+
for keys, code, label in _INDICATORS:
|
|
385
|
+
if any(k in low for k in keys):
|
|
386
|
+
ind, ind_label = code, label
|
|
387
|
+
break
|
|
388
|
+
return cc, ind, ind_label
|
|
389
|
+
|
|
390
|
+
def _fmt_title(cname: str, label: str, year: str, val: float, ind: str) -> str:
|
|
391
|
+
if ind == "SP.POP.TOTL":
|
|
392
|
+
return f"{cname} 总人口({year}):{val / 1e8:.2f} 亿人"
|
|
393
|
+
if ind == "NY.GDP.MKTP.CD":
|
|
394
|
+
return f"{cname} {label}({year}):{val / 1e12:.2f} 万亿美元"
|
|
395
|
+
if ind == "NY.GDP.PCAP.CD":
|
|
396
|
+
return f"{cname} 人均GDP({year}):{val / 1e4:.2f} 万美元"
|
|
397
|
+
if ind in ("FP.CPI.TOTL.ZG", "NY.GDP.MKTP.KD.ZG"):
|
|
398
|
+
return f"{cname} {label}({year}):{val:+.2f}%"
|
|
399
|
+
if ind == "SL.UEM.TOTL.ZS":
|
|
400
|
+
return f"{cname} 失业率({year}):{val:.2f}%"
|
|
401
|
+
if ind in ("NE.TRD.GNFS.ZS", "BN.CAB.XOKA.GD.ZS"):
|
|
402
|
+
return f"{cname} {label}({year}):{val:.2f}%"
|
|
403
|
+
if ind == "FR.INR.LEND":
|
|
404
|
+
return f"{cname} 贷款利率({year}):{val:.2f}%"
|
|
405
|
+
return f"{cname} {label}({year}):{val:,.2f}"
|
|
406
|
+
return _engine
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ── 国家统计局数据引擎(data.stats.gov.cn V2 API,免认证) ────────────────────
|
|
411
|
+
|
|
412
|
+
_NBS_PROVINCES: dict[str, str] = {
|
|
413
|
+
"北京": "110000000000", "天津": "120000000000", "河北": "130000000000",
|
|
414
|
+
"山西": "140000000000", "内蒙古": "150000000000", "辽宁": "210000000000",
|
|
415
|
+
"吉林": "220000000000", "黑龙江": "230000000000", "上海": "310000000000",
|
|
416
|
+
"江苏": "320000000000", "浙江": "330000000000", "安徽": "340000000000",
|
|
417
|
+
"福建": "350000000000", "江西": "360000000000", "山东": "370000000000",
|
|
418
|
+
"河南": "410000000000", "湖北": "420000000000", "湖南": "430000000000",
|
|
419
|
+
"广东": "440000000000", "广西": "450000000000", "海南": "460000000000",
|
|
420
|
+
"重庆": "500000000000", "四川": "510000000000", "贵州": "520000000000",
|
|
421
|
+
"云南": "530000000000", "西藏": "540000000000", "陕西": "610000000000",
|
|
422
|
+
"甘肃": "620000000000", "青海": "630000000000", "宁夏": "640000000000",
|
|
423
|
+
"新疆": "650000000000",
|
|
424
|
+
}
|
|
425
|
+
_NBS_PROVINCE_NAMES = tuple(_NBS_PROVINCES.keys())
|
|
426
|
+
|
|
427
|
+
# 指标关键词 → (搜索后缀, 全国搜索词, 显示名匹配子串)。顺序重要:人均类必须先于总量类匹配
|
|
428
|
+
_NBS_INDICATORS: list[tuple[tuple[str, ...], str, str, tuple[str, ...]]] = [
|
|
429
|
+
(("人均gdp", "人均生产总值", "人均国内生产总值"), "人均gdp", "人均gdp",
|
|
430
|
+
("人均地区生产总值", "人均国内生产总值")),
|
|
431
|
+
(("gdp", "生产总值", "国内生产总值", "经济总量"), "gdp", "国内生产总值",
|
|
432
|
+
("地区生产总值", "国内生产总值")),
|
|
433
|
+
(("cpi", "物价", "居民消费价格", "通货膨胀", "通胀"), "居民消费价格", "居民消费价格",
|
|
434
|
+
("居民消费价格指数",)),
|
|
435
|
+
(("ppi", "工业生产者", "生产者价格"), "工业生产者价格", "工业生产者价格",
|
|
436
|
+
("工业生产者出厂价格指数",)),
|
|
437
|
+
(("失业",), "失业", "失业", ("城镇登记失业", "城镇调查失业")),
|
|
438
|
+
(("就业",), "就业", "就业", ("就业人员",)),
|
|
439
|
+
(("人口",), "人口", "人口", ("人口数",)),
|
|
440
|
+
(("固定资产投资", "投资"), "固定资产投资", "固定资产投资", ("固定资产投资",)),
|
|
441
|
+
(("社会消费品零售", "零售"), "社会消费品零售", "社会消费品零售", ("社会消费品零售总额",)),
|
|
442
|
+
(("财政收入",), "财政收入", "财政收入", ("财政收入",)),
|
|
443
|
+
(("进出口", "出口", "外贸"), "进出口", "进出口", ("进出口",)),
|
|
444
|
+
(("m2", "货币供应"), "货币供应", "货币供应", ("货币供应",)),
|
|
445
|
+
(("pmi", "采购经理"), "采购经理", "采购经理", ("采购经理指数",)),
|
|
446
|
+
(("人均可支配", "可支配收入"), "可支配收入", "可支配收入", ("人均可支配收入",)),
|
|
447
|
+
(("出生", "生育"), "出生", "出生", ("出生人口",)),
|
|
448
|
+
(("死亡",), "死亡", "死亡", ("死亡人口",)),
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
# 显示名中的噪声词:指数/拉动/贡献率/环比/季度累计/不变价/构成等派生指标一律降级
|
|
452
|
+
_NBS_NOISE = ("指数", "拉动", "贡献", "环比", "当季", "累计", "不变价",
|
|
453
|
+
"支出法", "收入法", "构成", "1978", "1985", "定基", "增速", "增长")
|
|
454
|
+
|
|
455
|
+
_NBS_ROOT_ID = "69c574ab128a44e595cc0b24502b771b"
|
|
456
|
+
_NBS_API = "https://data.stats.gov.cn/dg/website/publicrelease/web/external"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _build_nbs_stats_engine(spec: dict[str, Any]) -> Any:
|
|
460
|
+
"""国家统计局数据(data.stats.gov.cn V2 API,免认证)。
|
|
461
|
+
|
|
462
|
+
三步走:搜索定位 cid(含省名才触发分省数据)→ esData 批量取数。
|
|
463
|
+
支持全国/单省/31省排行三类查询,GDP/CPI/PPI/人口/就业/投资等指标。
|
|
464
|
+
"""
|
|
465
|
+
timeout = spec.get("timeout", 12)
|
|
466
|
+
_HEADERS = {
|
|
467
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
468
|
+
"Referer": "https://data.stats.gov.cn/",
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
def _search(kw: str, to: float) -> list[dict]:
|
|
472
|
+
"""搜索指标元数据,返回原始条目列表。"""
|
|
473
|
+
url = f"{_NBS_API}/query?search={urllib.parse.quote(kw)}&pagenum=1&pageSize=15"
|
|
474
|
+
try:
|
|
475
|
+
req = urllib.request.Request(url, headers=_HEADERS)
|
|
476
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
477
|
+
d = json.loads(resp.read().decode("utf-8", "replace"))
|
|
478
|
+
except Exception as e:
|
|
479
|
+
logger.warning(f"统计局搜索失败: {e}")
|
|
480
|
+
return []
|
|
481
|
+
items = (d.get("data") or {}).get("data") or []
|
|
482
|
+
return [it for it in items if isinstance(it, dict) and it.get("cid")]
|
|
483
|
+
|
|
484
|
+
def _fetch(cid: str, indicator_ids: list[str], das: list[dict],
|
|
485
|
+
dts: list[str], to: float) -> list[dict]:
|
|
486
|
+
"""esData 批量取数,返回 [{code,name,values:[...]}]。"""
|
|
487
|
+
body = {
|
|
488
|
+
"cid": cid, "indicatorIds": indicator_ids, "das": das,
|
|
489
|
+
"dts": dts, "showType": "2", "rootId": _NBS_ROOT_ID,
|
|
490
|
+
}
|
|
491
|
+
try:
|
|
492
|
+
req = urllib.request.Request(
|
|
493
|
+
f"{_NBS_API}/stream/esData",
|
|
494
|
+
data=json.dumps(body).encode(),
|
|
495
|
+
headers={**_HEADERS, "Content-Type": "application/json"},
|
|
496
|
+
)
|
|
497
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
498
|
+
d = json.loads(resp.read().decode("utf-8", "replace"))
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.warning(f"统计局取数失败: {e}")
|
|
501
|
+
return []
|
|
502
|
+
data = d.get("data")
|
|
503
|
+
return data if isinstance(data, list) else []
|
|
504
|
+
|
|
505
|
+
@safe_search
|
|
506
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
507
|
+
import datetime as _dt
|
|
508
|
+
to = _timeout or timeout
|
|
509
|
+
q = query.strip()
|
|
510
|
+
low = q.lower()
|
|
511
|
+
if not low:
|
|
512
|
+
return []
|
|
513
|
+
|
|
514
|
+
# 1. 省份解析:单省 / 全部31省(各省、排名)/ 全国
|
|
515
|
+
province = ""
|
|
516
|
+
for name in _NBS_PROVINCE_NAMES:
|
|
517
|
+
if name in q:
|
|
518
|
+
province = name
|
|
519
|
+
break
|
|
520
|
+
all_prov = bool(province) and any(k in q for k in ("各省", "31省", "省份", "排名", "排行", "对比", "全部"))
|
|
521
|
+
if not province and any(k in q for k in ("各省", "31省", "省份", "全国各省")):
|
|
522
|
+
all_prov = True
|
|
523
|
+
|
|
524
|
+
# 2. 指标解析
|
|
525
|
+
suffix = national = ""
|
|
526
|
+
matches: tuple[str, ...] = ()
|
|
527
|
+
for keys, s, ns, ms in _NBS_INDICATORS:
|
|
528
|
+
if any(k in low for k in keys):
|
|
529
|
+
suffix, national, matches = s, ns, ms
|
|
530
|
+
break
|
|
531
|
+
if not suffix:
|
|
532
|
+
return []
|
|
533
|
+
|
|
534
|
+
# 3. 年份解析:指定年或默认最新(用搜索命中条目的年份)
|
|
535
|
+
ym = re.search(r"(20\d{2})\s*年?", q)
|
|
536
|
+
year = ym.group(1) if ym else ""
|
|
537
|
+
|
|
538
|
+
# 4. 搜索定位 cid:分省查询必须带省名才能触发分省数据
|
|
539
|
+
if province:
|
|
540
|
+
search_kw = f"{province}{suffix}"
|
|
541
|
+
elif all_prov:
|
|
542
|
+
search_kw = f"北京{suffix}"
|
|
543
|
+
else:
|
|
544
|
+
search_kw = national
|
|
545
|
+
items = _search(search_kw, to)
|
|
546
|
+
if not items:
|
|
547
|
+
return []
|
|
548
|
+
|
|
549
|
+
# 5. 从搜索结果选最佳条目:指标名命中 > 无噪声派生词 > 年度 > 地域匹配
|
|
550
|
+
def _score(it: dict) -> int:
|
|
551
|
+
sn = (it.get("show_name") or "").strip()
|
|
552
|
+
s = 0
|
|
553
|
+
if any(m in sn for m in matches):
|
|
554
|
+
s += 100
|
|
555
|
+
if any(m in sn[:8] for m in matches):
|
|
556
|
+
s += 30
|
|
557
|
+
if any(w in sn for w in _NBS_NOISE):
|
|
558
|
+
s -= 60
|
|
559
|
+
if "(上年=100)" in sn or "(上年同期=100)" in sn:
|
|
560
|
+
s += 20
|
|
561
|
+
if "分省" in (it.get("type_text") or ""):
|
|
562
|
+
s += 15
|
|
563
|
+
if "年度" in (it.get("type_text") or ""):
|
|
564
|
+
s += 20
|
|
565
|
+
elif "季度" in (it.get("type_text") or ""):
|
|
566
|
+
s -= 30
|
|
567
|
+
if province and not all_prov:
|
|
568
|
+
if (it.get("da_name") or "").startswith(province):
|
|
569
|
+
s += 40
|
|
570
|
+
elif not province and not all_prov:
|
|
571
|
+
if (it.get("da_name") or "") == "全国":
|
|
572
|
+
s += 40
|
|
573
|
+
return s
|
|
574
|
+
items.sort(key=_score, reverse=True)
|
|
575
|
+
hit = items[0]
|
|
576
|
+
cid = hit["cid"]
|
|
577
|
+
indicator_id = hit["indic_id"]
|
|
578
|
+
show_name = (hit.get("show_name") or "").strip()
|
|
579
|
+
hit_year = (hit.get("dt") or "")[:4]
|
|
580
|
+
if not year:
|
|
581
|
+
year = hit_year
|
|
582
|
+
|
|
583
|
+
# 6. 构造 das / dts
|
|
584
|
+
if all_prov:
|
|
585
|
+
das = [{"text": name, "value": code} for name, code in _NBS_PROVINCES.items()]
|
|
586
|
+
elif province:
|
|
587
|
+
das = [{"text": province, "value": _NBS_PROVINCES[province]}]
|
|
588
|
+
else:
|
|
589
|
+
das = [{"text": "全国", "value": "000000000000"}]
|
|
590
|
+
|
|
591
|
+
rows = _fetch(cid, [indicator_id], das, [f"{year}YY-{year}YY"], to)
|
|
592
|
+
if not rows and year != hit_year:
|
|
593
|
+
# 指定年份数据未发布时回退到命中条目的年份
|
|
594
|
+
year = hit_year
|
|
595
|
+
rows = _fetch(cid, [indicator_id], das, [f"{year}YY-{year}YY"], to)
|
|
596
|
+
if not rows:
|
|
597
|
+
return []
|
|
598
|
+
|
|
599
|
+
# 7. 格式化:31省按值排序逐条输出,单省/全国输出一条
|
|
600
|
+
if all_prov:
|
|
601
|
+
label = f"{year}年各省{show_name}"
|
|
602
|
+
elif province:
|
|
603
|
+
label = f"{province} {year}年 {show_name}"
|
|
604
|
+
else:
|
|
605
|
+
label = f"全国 {year}年 {show_name}"
|
|
606
|
+
results = []
|
|
607
|
+
if all_prov:
|
|
608
|
+
parsed = []
|
|
609
|
+
for it in rows:
|
|
610
|
+
vals = it.get("values") or []
|
|
611
|
+
if not vals:
|
|
612
|
+
continue
|
|
613
|
+
v = vals[0]
|
|
614
|
+
try:
|
|
615
|
+
fv = float(v.get("value"))
|
|
616
|
+
except (TypeError, ValueError):
|
|
617
|
+
continue
|
|
618
|
+
parsed.append((fv, it.get("name", "")))
|
|
619
|
+
parsed.sort(reverse=True)
|
|
620
|
+
du = (rows[0].get("values") or [{}])[0].get("du_name") or ""
|
|
621
|
+
if du == "无":
|
|
622
|
+
du = ""
|
|
623
|
+
for i, (fv, pname) in enumerate(parsed[: min(n, 31)], 1):
|
|
624
|
+
results.append({
|
|
625
|
+
"title": f"{i}. {pname} {show_name} {fv:,.1f} {du}",
|
|
626
|
+
"url": "https://data.stats.gov.cn/easyquery.htm",
|
|
627
|
+
"snippet": f"{label} 排行第{i}名 | 数据来源:国家统计局",
|
|
628
|
+
"source": "nbs_stats",
|
|
629
|
+
"score": rank_score(0.95, i),
|
|
630
|
+
})
|
|
631
|
+
else:
|
|
632
|
+
for _rk2, it in enumerate(rows):
|
|
633
|
+
vals = it.get("values") or []
|
|
634
|
+
if not vals:
|
|
635
|
+
continue
|
|
636
|
+
v = vals[0]
|
|
637
|
+
val = v.get("value")
|
|
638
|
+
du = v.get("du_name") or ""
|
|
639
|
+
if du == "无":
|
|
640
|
+
du = ""
|
|
641
|
+
dt_name = v.get("dt_name") or f"{year}年"
|
|
642
|
+
results.append({
|
|
643
|
+
"title": f"{label}: {val} {du}".strip(),
|
|
644
|
+
"url": "https://data.stats.gov.cn/easyquery.htm",
|
|
645
|
+
"snippet": f"{label} | 数据来源:国家统计局 | 统计周期 {dt_name}",
|
|
646
|
+
"source": "nbs_stats",
|
|
647
|
+
"score": rank_score(0.95, _rk2),
|
|
648
|
+
})
|
|
649
|
+
return results[: max(n, 5)]
|
|
650
|
+
|
|
651
|
+
return _engine
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# ── Eurostat 欧盟统计引擎(macro_data 域) ───────────────────────────────────
|
|
656
|
+
|
|
657
|
+
_EUROSTAT_GEO: dict[str, str] = {
|
|
658
|
+
"德国": "DE", "germany": "DE", "法国": "FR", "france": "FR",
|
|
659
|
+
"英国": "UK", "uk": "UK", "united kingdom": "UK", "意大利": "IT",
|
|
660
|
+
"italy": "IT", "西班牙": "ES", "spain": "ES", "荷兰": "NL",
|
|
661
|
+
"netherlands": "NL", "比利时": "BE", "belgium": "BE", "奥地利": "AT",
|
|
662
|
+
"austria": "AT", "爱尔兰": "IE", "ireland": "IE", "葡萄牙": "PT",
|
|
663
|
+
"portugal": "PT", "希腊": "EL", "greece": "EL", "芬兰": "FI",
|
|
664
|
+
"finland": "FI", "瑞典": "SE", "sweden": "SE", "丹麦": "DK",
|
|
665
|
+
"denmark": "DK", "波兰": "PL", "poland": "PL", "捷克": "CZ",
|
|
666
|
+
"czech": "CZ", "匈牙利": "HU", "hungary": "HU", "罗马尼亚": "RO",
|
|
667
|
+
"romania": "RO", "保加利亚": "BG", "bulgaria": "BG",
|
|
668
|
+
"斯洛伐克": "SK", "slovakia": "SK", "斯洛文尼亚": "SI",
|
|
669
|
+
"slovenia": "SI", "克罗地亚": "HR", "croatia": "HR",
|
|
670
|
+
"立陶宛": "LT", "lithuania": "LT", "拉脱维亚": "LV", "latvia": "LV",
|
|
671
|
+
"爱沙尼亚": "EE", "estonia": "EE", "塞浦路斯": "CY", "cyprus": "CY",
|
|
672
|
+
"卢森堡": "LU", "luxembourg": "LU", "马耳他": "MT", "malta": "MT",
|
|
673
|
+
"欧盟": "EU27_2020", "eu": "EU27_2020", "european union": "EU27_2020",
|
|
674
|
+
"欧元区": "EA20", "eurozone": "EA20", "euro area": "EA20",
|
|
675
|
+
}
|
|
676
|
+
# 指标关键词 → (数据集, 维度参数, 指标名)。维度均已实测验证
|
|
677
|
+
_EUROSTAT_INDICATORS: list[tuple[tuple[str, ...], str, str, str]] = [
|
|
678
|
+
(("人均gdp", "人均生产总值", "人均国内生产总值", "gdp per capita", "gdp/person"),
|
|
679
|
+
"nama_10_pc", "na_item=B1GQ&unit=CP_EUR_HAB", "人均GDP"),
|
|
680
|
+
(("gdp", "生产总值", "国内生产总值", "经济总量"), "nama_10_gdp", "na_item=B1GQ&unit=CP_MEUR", "GDP"),
|
|
681
|
+
(("失业", "失业率", "unemployment", "jobless"), "une_rt_a", "age=Y15-74&sex=T&unit=PC_ACT", "失业率"),
|
|
682
|
+
(("人口", "population", "inhabitants"), "demo_pjan", "age=TOTAL&sex=T", "人口"),
|
|
683
|
+
]
|
|
684
|
+
# unit 编码 → 简短单位标签(未命中时回退 API 原文)
|
|
685
|
+
_EUROSTAT_UNIT: dict[str, str] = {
|
|
686
|
+
"CP_MEUR": "百万欧元", "CP_EUR_HAB": "欧元/人", "PC_ACT": "%", "PERSON": "人",
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _build_eurostat_engine(spec: dict[str, Any]) -> Any:
|
|
691
|
+
"""欧盟统计局 Eurostat 数据(ec.europa.eu SDMX 2.1,免认证)。
|
|
692
|
+
|
|
693
|
+
支持 EU 国家/欧盟/欧元区的 GDP、人均GDP、失业率、人口年度查询。
|
|
694
|
+
"""
|
|
695
|
+
timeout = spec.get("timeout", 15)
|
|
696
|
+
_HEADERS = {"User-Agent": "argo-search/2.6 (unified-search@local)", "Accept": "application/json"}
|
|
697
|
+
_BASE = "https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data"
|
|
698
|
+
|
|
699
|
+
def _jget(url: str, to: float) -> dict:
|
|
700
|
+
req = urllib.request.Request(url, headers=_HEADERS)
|
|
701
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
702
|
+
return json.loads(resp.read().decode("utf-8", "replace"))
|
|
703
|
+
|
|
704
|
+
@safe_search
|
|
705
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
706
|
+
q = query.strip()
|
|
707
|
+
if not q:
|
|
708
|
+
return []
|
|
709
|
+
to = _timeout or timeout
|
|
710
|
+
low = q.lower()
|
|
711
|
+
|
|
712
|
+
country = _match_country(q, _EUROSTAT_GEO)
|
|
713
|
+
if not country:
|
|
714
|
+
return []
|
|
715
|
+
|
|
716
|
+
ds = dims = label = ""
|
|
717
|
+
for keys, d, dm, lb in _EUROSTAT_INDICATORS:
|
|
718
|
+
if any(k in low for k in keys):
|
|
719
|
+
ds, dims, label = d, dm, lb
|
|
720
|
+
break
|
|
721
|
+
if not ds:
|
|
722
|
+
return []
|
|
723
|
+
|
|
724
|
+
ym = re.search(r"(20\d{2})", q)
|
|
725
|
+
year = ym.group(1) if ym else ""
|
|
726
|
+
|
|
727
|
+
url = f"{_BASE}/{ds}?format=JSON&lang=en&geo={country}&{dims}"
|
|
728
|
+
if year:
|
|
729
|
+
url += f"&time={year}"
|
|
730
|
+
try:
|
|
731
|
+
d = _jget(url, to)
|
|
732
|
+
except Exception as e:
|
|
733
|
+
logger.warning(f"Eurostat 取数失败: {e}")
|
|
734
|
+
return []
|
|
735
|
+
vals = d.get("value") or {}
|
|
736
|
+
if not vals:
|
|
737
|
+
return []
|
|
738
|
+
# 最新观测
|
|
739
|
+
v = list(vals.values())[0]
|
|
740
|
+
dims_meta = d.get("dimension", {})
|
|
741
|
+
if not year:
|
|
742
|
+
# 无年份时取最新观测:value 键为观测索引(按时间升序),映射回时间标签
|
|
743
|
+
t_idx = max(vals.keys(), key=lambda k: int(k) if str(k).isdigit() else 0)
|
|
744
|
+
t_cat = (dims_meta.get("time") or {}).get("category", {})
|
|
745
|
+
t_code = next((c for c, i in (t_cat.get("index") or {}).items()
|
|
746
|
+
if str(i) == str(t_idx)), "")
|
|
747
|
+
year = (t_cat.get("label") or {}).get(t_code, t_code)[:4]
|
|
748
|
+
# 单位标签:unit 维度 category 的 key 是编码(如 CP_MEUR),先查短标签表再回退 API 标签
|
|
749
|
+
du = ""
|
|
750
|
+
try:
|
|
751
|
+
unit_cat = (dims_meta.get("unit") or {}).get("category", {})
|
|
752
|
+
ucode = next(iter((unit_cat.get("index") or {}).keys()), "")
|
|
753
|
+
du = _EUROSTAT_UNIT.get(ucode) or (unit_cat.get("label") or {}).get(ucode, "")
|
|
754
|
+
except Exception:
|
|
755
|
+
pass
|
|
756
|
+
cname = next((k for k, v in _EUROSTAT_GEO.items() if v == country), country)
|
|
757
|
+
try:
|
|
758
|
+
fv = float(v)
|
|
759
|
+
val_s = f"{fv:,.1f}"
|
|
760
|
+
except (TypeError, ValueError):
|
|
761
|
+
val_s = str(v)
|
|
762
|
+
title = f"{cname} {year}年 {label}: {val_s} {du}".strip()
|
|
763
|
+
results = [{
|
|
764
|
+
"title": title,
|
|
765
|
+
"url": f"https://ec.europa.eu/eurostat/databrowser/view/{ds}",
|
|
766
|
+
"snippet": f"{cname} {label}({year})| 数据来源:Eurostat | 数据集 {ds}",
|
|
767
|
+
"source": "eurostat",
|
|
768
|
+
"score": 0.93,
|
|
769
|
+
}]
|
|
770
|
+
return results[: max(n, 3)]
|
|
771
|
+
|
|
772
|
+
return _engine
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
|