argo-search 1.0.1 → 2.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/README.en.md +735 -0
- package/README.es.md +777 -0
- package/README.ja.md +777 -0
- package/README.ko.md +777 -0
- package/README.md +778 -320
- package/SKILL.md +135 -0
- package/assets/readme/hero.svg +121 -0
- package/assets/readme/made-with-beautify.svg +10 -0
- package/assets/readme/proof-routes.svg +73 -0
- package/assets/readme/why-better.svg +104 -0
- package/assets/readme/workflow.svg +83 -0
- package/backends/domain_profiles.json +2356 -96
- package/backends/engine_registry.yaml +3428 -503
- package/backends/query_synonyms_cn.json +35 -0
- package/backends/quota_profiles.json +2488 -160
- package/backends/source_types_cn.json +109 -0
- package/backends/text_lexicon.json +44 -0
- package/bin/argo +772 -0
- package/bin/argo.js +102 -39
- package/config.yaml +5139 -367
- package/cordis.patch.yml +21 -0
- package/data/countries.json +40 -0
- package/data/lang_matrix/lang_capability.json +842 -0
- package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
- package/data/regions_cn.json +1 -0
- package/docs/ADDING_NEW_ENGINE.md +184 -0
- package/docs/ARGO_INTRO.md +124 -0
- package/docs/DATA_SOURCE_GAPS.md +81 -0
- package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
- package/docs/ENGINE_CATALOG.md +575 -0
- package/docs/MCP_SETUP.md +108 -0
- package/docs/PLATFORM_PATHS.md +111 -0
- package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
- package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
- package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
- package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
- package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
- package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
- package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
- package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
- package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
- package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
- package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
- package/docs/SEARCH_ARCHIVE.md +115 -0
- package/docs/assets/architecture.svg +104 -0
- package/docs/assets/hero.svg +98 -0
- package/docs/engines/bangumi.md +27 -0
- package/docs/engines/biorxiv.md +27 -0
- package/docs/engines/clawhub.md +27 -0
- package/docs/engines/crt_sh.md +27 -0
- package/docs/engines/datacite.md +27 -0
- package/docs/engines/deps_dev.md +27 -0
- package/docs/engines/douban_movie.md +27 -0
- package/docs/engines/endoflife.md +27 -0
- package/docs/engines/flk_law.md +27 -0
- package/docs/engines/fxtwitter.md +27 -0
- package/docs/engines/google_news.md +27 -0
- package/docs/engines/gov_regulations.md +27 -0
- package/docs/engines/guqian.md +27 -0
- package/docs/engines/hackernews.md +27 -0
- package/docs/engines/iplant.md +27 -0
- package/docs/engines/keenable.md +27 -0
- package/docs/engines/met_museum.md +27 -0
- package/docs/engines/nasa_images.md +27 -0
- package/docs/engines/netease_music.md +27 -0
- package/docs/engines/npm.md +27 -0
- package/docs/engines/nvd.md +27 -0
- package/docs/engines/openreview.md +27 -0
- package/docs/engines/openstd.md +27 -0
- package/docs/engines/parallel_free.md +27 -0
- package/docs/engines/people_daily.md +27 -0
- package/docs/engines/realtime_index.md +27 -0
- package/docs/engines/sec_edgar.md +27 -0
- package/docs/engines/seltz.md +27 -0
- package/docs/engines/skillsmp.md +27 -0
- package/docs/engines/sspai.md +27 -0
- package/docs/engines/std_samr.md +27 -0
- package/docs/engines/train.md +27 -0
- package/docs/engines/un_comtrade.md +27 -0
- package/docs/engines/wikisource.md +27 -0
- package/docs/engines/zdic.md +27 -0
- package/docs/engines/zenodo.md +27 -0
- package/docs/engines/zhihu_hot_app.md +27 -0
- package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
- package/engines/README.md +10 -0
- package/engines/_template_cli.yaml +39 -0
- package/engines/_template_http.yaml +37 -0
- package/engines/plugins/_template_plugin.py +50 -0
- package/engines/specs/_cisa_kev.yaml +54 -0
- package/engines/specs/_osv.yaml +63 -0
- package/engines/specs/aljazeera.yaml +43 -0
- package/engines/specs/antara.yaml +43 -0
- package/engines/specs/artic.yaml +20 -0
- package/engines/specs/aviation_weather.yaml +39 -0
- package/engines/specs/bangumi.yaml +21 -0
- package/engines/specs/biorxiv.yaml +24 -0
- package/engines/specs/carbon_intensity.yaml +32 -0
- package/engines/specs/cfets.yaml +41 -0
- package/engines/specs/cinii.yaml +44 -0
- package/engines/specs/clawhub.yaml +49 -0
- package/engines/specs/cleveland.yaml +19 -0
- package/engines/specs/cn_ai_news.yaml +43 -0
- package/engines/specs/core.yaml +35 -0
- package/engines/specs/crt_sh.yaml +38 -0
- package/engines/specs/ct24.yaml +43 -0
- package/engines/specs/datacite.yaml +27 -0
- package/engines/specs/datagov_uk.yaml +41 -0
- package/engines/specs/ddgs_images.yaml +45 -0
- package/engines/specs/deepseek_search.yaml +74 -0
- package/engines/specs/deezer.yaml +20 -0
- package/engines/specs/defillama.yaml +38 -0
- package/engines/specs/deps_dev.yaml +21 -0
- package/engines/specs/digup.yaml +36 -0
- package/engines/specs/doi.yaml +39 -0
- package/engines/specs/douban_movie.yaml +22 -0
- package/engines/specs/dw_news.yaml +47 -0
- package/engines/specs/egov_law.yaml +21 -0
- package/engines/specs/elpais.yaml +43 -0
- package/engines/specs/endoflife.yaml +21 -0
- package/engines/specs/energy_charts.yaml +21 -0
- package/engines/specs/factcheck_org.yaml +52 -0
- package/engines/specs/faz.yaml +43 -0
- package/engines/specs/federal_register.yaml +57 -0
- package/engines/specs/firecrawl.yaml +53 -0
- package/engines/specs/flk_law.yaml +24 -0
- package/engines/specs/folha.yaml +43 -0
- package/engines/specs/france24.yaml +44 -0
- package/engines/specs/full_fact.yaml +47 -0
- package/engines/specs/fxtwitter.yaml +36 -0
- package/engines/specs/gbfs_nyc.yaml +32 -0
- package/engines/specs/gdacs.yaml +21 -0
- package/engines/specs/gdelt.yaml +64 -0
- package/engines/specs/gleif.yaml +55 -0
- package/engines/specs/google_news.yaml +22 -0
- package/engines/specs/gov_regulations.yaml +38 -0
- package/engines/specs/guardian_rss.yaml +59 -0
- package/engines/specs/hkex.yaml +40 -0
- package/engines/specs/hurriyet.yaml +43 -0
- package/engines/specs/iplant.yaml +25 -0
- package/engines/specs/jikan.yaml +24 -0
- package/engines/specs/jolpica.yaml +20 -0
- package/engines/specs/k10plus.yaml +20 -0
- package/engines/specs/keenable.yaml +44 -0
- package/engines/specs/keenable_free.yaml +36 -0
- package/engines/specs/lefigaro.yaml +43 -0
- package/engines/specs/listenbrainz.yaml +19 -0
- package/engines/specs/matichon.yaml +43 -0
- package/engines/specs/met_museum.yaml +21 -0
- package/engines/specs/nasa_images.yaml +45 -0
- package/engines/specs/netease_music.yaml +38 -0
- package/engines/specs/nhk.yaml +43 -0
- package/engines/specs/nhtsa_vpic.yaml +20 -0
- package/engines/specs/noaa_swpc.yaml +20 -0
- package/engines/specs/nvd.yaml +38 -0
- package/engines/specs/obis.yaml +20 -0
- package/engines/specs/opencitations.yaml +52 -0
- package/engines/specs/openf1.yaml +21 -0
- package/engines/specs/openfoodfacts.yaml +45 -0
- package/engines/specs/openligadb.yaml +20 -0
- package/engines/specs/openreview.yaml +37 -0
- package/engines/specs/openstd.yaml +21 -0
- package/engines/specs/people_daily.yaml +22 -0
- package/engines/specs/realtime_index.yaml +34 -0
- package/engines/specs/ror.yaml +20 -0
- package/engines/specs/satnogs.yaml +20 -0
- package/engines/specs/sec_edgar.yaml +46 -0
- package/engines/specs/shfe.yaml +39 -0
- package/engines/specs/skillsmp.yaml +50 -0
- package/engines/specs/sogou_weixin_browser.yaml +30 -0
- package/engines/specs/soilgrids.yaml +25 -0
- package/engines/specs/sspai.yaml +36 -0
- package/engines/specs/stackexchange.yaml +46 -0
- package/engines/specs/std_samr.yaml +21 -0
- package/engines/specs/tass.yaml +43 -0
- package/engines/specs/tavily_free.yaml +56 -0
- package/engines/specs/tle_mirror.yaml +21 -0
- package/engines/specs/train.yaml +37 -0
- package/engines/specs/tvmaze.yaml +20 -0
- package/engines/specs/twitter_syndication.yaml +31 -0
- package/engines/specs/un_comtrade.yaml +23 -0
- package/engines/specs/unpaywall.yaml +54 -0
- package/engines/specs/vnpress.yaml +43 -0
- package/engines/specs/weather.yaml +32 -0
- package/engines/specs/who_don.yaml +21 -0
- package/engines/specs/who_gho.yaml +20 -0
- package/engines/specs/wikimedia_commons.yaml +34 -0
- package/engines/specs/wikisource.yaml +22 -0
- package/engines/specs/worms.yaml +19 -0
- package/engines/specs/x_trending.yaml +34 -0
- package/engines/specs/yna.yaml +43 -0
- package/engines/specs/ynet.yaml +43 -0
- package/engines/specs/yuanbao.yaml +93 -0
- package/engines/specs/zbmath.yaml +40 -0
- package/engines/specs/zdic.yaml +22 -0
- package/engines/specs/zenodo.yaml +27 -0
- package/engines/specs/zhihu_hot_app.yaml +35 -0
- package/mcp/clients.yaml +106 -0
- package/package.json +55 -12
- package/packages/dsh-plugin/README.md +152 -0
- package/packages/dsh-plugin/cordis.patch.yml +48 -0
- package/packages/dsh-plugin/dsh/client.js +1533 -0
- package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
- package/packages/dsh-plugin/dsh/index.js +527 -0
- package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +38 -0
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +406 -0
- package/packages/dsh-plugin/test/config.test.js +30 -0
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +1382 -0
- package/references/query-templates.md +96 -0
- package/references/query-templates.yaml +118 -0
- package/scripts/ab_eval.py +219 -0
- package/scripts/ab_eval_p0p1.py +266 -0
- package/scripts/adaptive.py +294 -70
- package/scripts/answer.py +154 -0
- package/scripts/archive_run.py +591 -0
- package/scripts/argo_engine_registry.py +172 -0
- package/scripts/argo_paths.py +728 -0
- package/scripts/article.py +136 -0
- package/scripts/article_meta.py +131 -0
- package/scripts/arxiv_source.py +201 -0
- package/scripts/badcase_ledger.py +252 -0
- package/scripts/batch_probe.py +241 -0
- package/scripts/benchmark.py +19 -0
- package/scripts/bounded_run.py +144 -0
- package/scripts/browser_auth.py +323 -0
- package/scripts/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/burstiness_detector.py +147 -0
- package/scripts/cache.py +1066 -67
- package/scripts/cache_guard.py +271 -0
- package/scripts/cache_key_vdom.py +56 -0
- package/scripts/cache_similarity.py +170 -0
- package/scripts/caliber_isolation.py +195 -0
- package/scripts/calibrate_lowquality.py +210 -0
- package/scripts/candidate_envelope.py +366 -0
- package/scripts/chrome_cdp.py +871 -0
- package/scripts/ci_drift_gate.py +90 -0
- package/scripts/circuit_breaker.py +389 -0
- package/scripts/citations.py +334 -0
- package/scripts/cite_cli.py +59 -0
- package/scripts/cjk_tokens.py +95 -0
- package/scripts/clarify.py +47 -24
- package/scripts/cli_io.py +240 -0
- package/scripts/config.py +683 -45
- package/scripts/config_cache.py +402 -0
- package/scripts/conn_pool.py +220 -0
- package/scripts/content_boundary.py +46 -0
- package/scripts/content_security.py +685 -0
- package/scripts/content_signals.py +751 -0
- package/scripts/crawl.py +74 -35
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/daemon.py +866 -0
- package/scripts/digup_bridge.py +214 -0
- package/scripts/dispatch_result.py +98 -0
- package/scripts/engine_admission.py +366 -0
- package/scripts/engine_dispatch.py +982 -0
- package/scripts/engine_env.py +539 -0
- package/scripts/engine_failure.py +411 -0
- package/scripts/engine_families.py +738 -0
- package/scripts/engine_policy.py +218 -0
- package/scripts/engine_requires.py +190 -0
- package/scripts/engine_status.py +322 -0
- package/scripts/engine_validate.py +638 -0
- package/scripts/engines.py +799 -294
- package/scripts/engines_base.py +1521 -0
- package/scripts/engines_builders.py +310 -0
- package/scripts/engines_builders_batch10.py +204 -0
- package/scripts/engines_builders_batch11.py +174 -0
- package/scripts/engines_builders_batch12.py +103 -0
- package/scripts/engines_builders_batch13.py +340 -0
- package/scripts/engines_builders_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -0
- package/scripts/engines_builders_batch9.py +1468 -0
- package/scripts/engines_builders_cn.py +2283 -0
- package/scripts/engines_builders_data.py +2923 -0
- package/scripts/engines_builders_data_macro.py +775 -0
- package/scripts/engines_builders_feeds.py +255 -0
- package/scripts/engines_builders_intl.py +1239 -0
- package/scripts/engines_builders_keyless.py +111 -0
- package/scripts/engines_builders_search.py +440 -0
- package/scripts/engines_builders_tech.py +1359 -0
- package/scripts/evidence.py +620 -166
- package/scripts/evidence_loop.py +624 -0
- package/scripts/evidence_tier.py +298 -0
- package/scripts/except_sets.py +25 -0
- package/scripts/extract.py +55 -16
- package/scripts/fact_align.py +207 -0
- package/scripts/fact_crosscheck.py +230 -0
- package/scripts/fetch.py +25 -12
- package/scripts/fetch_browser.py +87 -0
- package/scripts/fetch_jsrun.py +122 -0
- package/scripts/fetch_quality.py +225 -0
- package/scripts/fetch_render_tinyfish.py +117 -0
- package/scripts/fetch_v3.py +1804 -0
- package/scripts/focus_extract.py +350 -0
- package/scripts/fulltext_store.py +169 -0
- package/scripts/fxtwitter_api.py +325 -0
- package/scripts/gen_engine_catalog.py +364 -0
- package/scripts/gen_native_tools.py +91 -0
- package/scripts/gen_routing_snapshot.py +195 -0
- package/scripts/health_check.py +140 -402
- package/scripts/health_probe.py +23 -8
- package/scripts/hot_state.py +134 -0
- package/scripts/http_client.py +1053 -0
- package/scripts/image_ops.py +430 -0
- package/scripts/image_vision/vision_probe.swift +126 -0
- package/scripts/install.ps1 +166 -0
- package/scripts/install.sh +221 -0
- package/scripts/job.py +1186 -0
- package/scripts/jump_resolver.py +235 -0
- package/scripts/lang_capability.py +150 -0
- package/scripts/lang_detect.py +289 -0
- package/scripts/lang_matrix_probe.py +329 -0
- package/scripts/lang_pref.py +311 -0
- package/scripts/link_source.py +340 -0
- package/scripts/local_image.py +970 -0
- package/scripts/local_seek.py +402 -0
- package/scripts/macro_countries.py +97 -0
- package/scripts/matrix_search_eval.py +1187 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_diag.py +8 -3
- package/scripts/mcp_handlers.py +492 -0
- package/scripts/mcp_handlers_surface.py +157 -0
- package/scripts/mcp_launch.sh +124 -0
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +125 -419
- package/scripts/mcp_setup.py +431 -0
- package/scripts/mcp_tools.py +331 -0
- package/scripts/mcp_transport.py +191 -0
- package/scripts/mcp_warm.py +127 -0
- package/scripts/min_version_scan.py +335 -0
- package/scripts/net_proxy.py +258 -0
- package/scripts/network_aware.py +152 -0
- package/scripts/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -0
- package/scripts/paper.py +274 -0
- package/scripts/pdf_extract.py +518 -0
- package/scripts/phrase_filter.py +66 -0
- package/scripts/plan.py +422 -0
- package/scripts/postprocess_signals.py +150 -0
- package/scripts/pre-commit +48 -0
- package/scripts/query_enhance.py +157 -0
- package/scripts/query_operators.py +196 -0
- package/scripts/query_rewriter.py +116 -7
- package/scripts/query_signals.py +300 -0
- package/scripts/query_templates.py +167 -0
- package/scripts/query_understanding.py +411 -0
- package/scripts/query_variants.py +298 -0
- package/scripts/quota.py +633 -76
- package/scripts/rank_signals.py +775 -0
- package/scripts/ranking_eval.py +319 -0
- package/scripts/readability_extract.py +433 -0
- package/scripts/recompute.py +415 -0
- package/scripts/recovery.py +616 -0
- package/scripts/recovery_l0.py +160 -0
- package/scripts/recovery_runner.py +150 -0
- package/scripts/redskill/redskill_engine.py +332 -0
- package/scripts/regression_p0p1.py +318 -0
- package/scripts/replay_eval.py +658 -0
- package/scripts/rerank_tfidf.py +225 -0
- package/scripts/research.py +607 -403
- package/scripts/research_cli.py +361 -0
- package/scripts/research_dossier.py +492 -0
- package/scripts/research_expand.py +169 -0
- package/scripts/research_gates.py +272 -0
- package/scripts/research_report.py +209 -0
- package/scripts/research_strategy.py +37 -0
- package/scripts/research_work_packages.py +195 -0
- package/scripts/result_lang.py +311 -0
- package/scripts/retry_policy.py +76 -0
- package/scripts/robots_guard.py +259 -0
- package/scripts/route.py +921 -254
- package/scripts/route_bangs.py +40 -0
- package/scripts/route_cache.py +243 -0
- package/scripts/route_combo.py +917 -0
- package/scripts/route_domains.py +341 -0
- package/scripts/route_lang.py +605 -0
- package/scripts/route_log.py +63 -0
- package/scripts/route_policy.py +250 -0
- package/scripts/rrf_classic.py +66 -0
- package/scripts/screenshot.py +73 -0
- package/scripts/search.py +886 -406
- package/scripts/search_attribution.py +185 -0
- package/scripts/search_benchmark.py +407 -0
- package/scripts/search_cli.py +640 -0
- package/scripts/search_entry.py +395 -0
- package/scripts/search_output.py +690 -0
- package/scripts/search_pipeline.py +926 -0
- package/scripts/search_rank.py +991 -0
- package/scripts/search_types.py +129 -16
- package/scripts/seek_locator.py +68 -0
- package/scripts/semantic_evidence.py +391 -0
- package/scripts/serp_guard.py +336 -0
- package/scripts/serp_kill_log.py +74 -0
- package/scripts/single_flight.py +120 -0
- package/scripts/social_engines/__init__.py +10 -0
- package/scripts/social_engines/bilibili_engine.py +131 -0
- package/scripts/social_engines/hackernews_engine.py +113 -0
- package/scripts/social_engines/reddit_engine.py +172 -0
- package/scripts/social_engines/twitter_engine.py +397 -0
- package/scripts/social_engines/v2ex_engine.py +101 -0
- package/scripts/social_engines/weibo_engine.py +128 -0
- package/scripts/social_engines/xiaohongshu_engine.py +137 -0
- package/scripts/social_engines/zhihu_engine.py +139 -0
- package/scripts/social_research.py +201 -0
- package/scripts/sogou_weixin_browser.py +318 -0
- package/scripts/source_ledger.py +249 -0
- package/scripts/stage_timing.py +58 -0
- package/scripts/stats_cli.py +107 -0
- package/scripts/straggler_policy.py +60 -0
- package/scripts/stylometry_detector.py +231 -0
- package/scripts/sync_backends.py +353 -0
- package/scripts/sync_installs.py +29 -0
- package/scripts/template_cli.py +127 -0
- package/scripts/text_lexicon.py +72 -0
- package/scripts/tfidf_router.py +109 -37
- package/scripts/time_utils.py +233 -0
- package/scripts/topic_research_profiles.py +643 -0
- package/scripts/train.py +737 -0
- package/scripts/tweet.py +596 -0
- package/scripts/url_canon.py +217 -0
- package/scripts/url_optimize.py +56 -0
- package/scripts/url_safety.py +236 -0
- package/scripts/usage_log.py +116 -0
- package/scripts/usage_log_clean.py +113 -0
- package/scripts/v2ex_nodes.py +311 -0
- package/scripts/variant_recall.py +188 -0
- package/scripts/watch.py +205 -0
- package/scripts/wechat_article.py +258 -0
- package/scripts/wide_research.py +447 -0
- package/scripts/wx.py +349 -0
- package/scripts/x_article_fetch.py +141 -0
- package/scripts/x_search.py +234 -0
- package/scripts/x_trending.py +189 -0
- package/scripts/yaml_load.py +67 -0
- package/scripts/zhihu_quota.py +139 -0
- package/sub-skills/ego-search/SKILL.md +253 -0
- package/sub-skills/ego-search/scripts/ego_search.py +800 -0
- package/sub-skills/ego-search/scripts/merge.py +226 -0
- package/sub-skills/ego-search/scripts/quality.py +64 -0
- package/sub-skills/ego-search/scripts/runtime.py +172 -0
- package/sub-skills/ego-search/scripts/safety.py +109 -0
- package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
- package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
- package/sub-skills/js-run/SKILL.md +67 -0
- package/sub-skills/js-run/scripts/jsrun.py +112 -0
- package/sub-skills/js-run/scripts/shim.js +188 -0
- package/sub-skills/local-search/SKILL.md +50 -53
- package/sub-skills/local-search/bm25.py +186 -0
- package/sub-skills/local-search/cli_runner.py +331 -0
- package/sub-skills/local-search/config.yaml +152 -55
- package/sub-skills/local-search/engine_registry.py +91 -26
- package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
- package/sub-skills/local-search/local_search_adapter.py +13 -2
- package/sub-skills/local-search/parse_maps.yaml +40 -42
- package/sub-skills/local-search/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +716 -242
- package/sub-skills/local-search/smart_router.py +47 -7
- package/sub-skills/local-seek/SKILL.md +110 -0
- package/sub-skills/local-seek/config/domains.yaml +20 -0
- package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
- package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
- package/sub-skills/local-seek/scripts/seek.py +1465 -0
- package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
- package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
- package/sub-skills/local-seek/scripts/seek_rank.py +197 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""批次十四构建器:港股披露 / 商品期货 / 人民币中间价 / 链上 TVL(2026-10-21)。
|
|
3
|
+
|
|
4
|
+
hkex 港交所披露易(HKEXnews)公告检索:代码/公司名解析 + 标题关键词
|
|
5
|
+
shfe 上海期货交易所日行情(铜/铝/黄金/螺纹钢等,按品种过滤)
|
|
6
|
+
cfets 中国外汇交易中心人民币中间价(25 个币种,官方口径)
|
|
7
|
+
defillama DeFiLlama 协议 TVL(链上资金规模/24h 与 7d 变化)
|
|
8
|
+
|
|
9
|
+
四个源都是免 key 的官方/一手接口,补的是既有覆盖的结构性空白:
|
|
10
|
+
港股公告(A 股有巨潮、美股有 SEC,港股此前没有)、商品期货(此前零源)、
|
|
11
|
+
人民币中间价(此前只有 ECB/聚合汇率,没有在岸官方价)、链上 TVL(此前只有
|
|
12
|
+
CoinGecko 币价,没有协议资金规模)。
|
|
13
|
+
|
|
14
|
+
共同约束:
|
|
15
|
+
- 上游都不是「关键词全文检索」,而是「按标的枚举」——查询先做标的解析
|
|
16
|
+
(代码/品种/币种/协议名),解析不出来就诚实返回空,不猜、不拿全量
|
|
17
|
+
数据冒充结果。
|
|
18
|
+
- 全部走 http_open(失败现场归因),异常由 safe_search 兜底成空结果。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
import urllib.parse
|
|
27
|
+
import urllib.request
|
|
28
|
+
from datetime import datetime, timedelta, timezone
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from engines_base import http_open, safe_search
|
|
32
|
+
|
|
33
|
+
_UA = "argo-search/2.9 (unified-search@local)"
|
|
34
|
+
_CST = timezone(timedelta(hours=8)) # 三个中国市场源都按北京时间取「今天」
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _today_cst() -> datetime:
|
|
38
|
+
return datetime.now(_CST)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _json_open(url: str, timeout: float, engine: str,
|
|
42
|
+
headers: dict[str, str] | None = None) -> Any:
|
|
43
|
+
hdr = {"User-Agent": _UA, "Accept": "application/json"}
|
|
44
|
+
hdr.update(headers or {})
|
|
45
|
+
req = urllib.request.Request(url, headers=hdr)
|
|
46
|
+
with http_open(req, timeout=timeout, engine=engine) as resp:
|
|
47
|
+
return json.loads(resp.read().decode("utf-8", "replace"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── 港交所披露易(HKEXnews)──────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
# 披露易文档标题是繁体中文, mainland 用户习惯输入简体;不做整表转换
|
|
53
|
+
# (引入 OpenCC 太重),只覆盖港股公告里的高频词。未命中的简体词仍可通过
|
|
54
|
+
# 代码/公司名解析路径命中,不会整源失效。
|
|
55
|
+
_HKEX_S2T_CHARS = str.maketrans({
|
|
56
|
+
"腾": "騰", "讯": "訊", "汇": "匯", "丰": "豐", "团": "團", "亚": "亞",
|
|
57
|
+
"银": "銀", "证": "證", "产": "產", "业": "業", "务": "務", "国": "國",
|
|
58
|
+
"际": "際", "华": "華", "东": "東", "电": "電", "车": "車", "险": "險",
|
|
59
|
+
"药": "藥", "医": "醫", "疗": "療", "网": "網", "络": "絡", "软": "軟",
|
|
60
|
+
"数": "數", "据": "據", "显": "顯", "传": "傳", "铁": "鐵", "钢": "鋼",
|
|
61
|
+
"铝": "鋁", "铜": "銅", "矿": "礦", "黄": "黃", "锂": "鋰", "风": "風",
|
|
62
|
+
"环": "環", "气": "氣", "农": "農", "养": "養", "饲": "飼", "纺": "紡",
|
|
63
|
+
"织": "織", "装": "裝", "递": "遞", "线": "線", "导": "導", "体": "體",
|
|
64
|
+
"制": "製", "筑": "築", "饰": "飾", "轮": "輪", "纸": "紙", "贸": "貿",
|
|
65
|
+
"资": "資", "权": "權", "兴": "興", "发": "發", "长": "長", "龙": "龍",
|
|
66
|
+
"马": "馬", "湾": "灣", "门": "門", "开": "開", "关": "關", "报": "報",
|
|
67
|
+
"总": "總", "结": "結", "构": "構", "变": "變", "调": "調", "库": "庫",
|
|
68
|
+
"检": "檢", "测": "測", "试": "試", "验": "驗", "认": "認", "书": "書",
|
|
69
|
+
"许": "許", "营": "營", "销": "銷", "价": "價", "额": "額", "页": "頁",
|
|
70
|
+
"码": "碼", "视": "視", "频": "頻", "图": "圖", "买": "買", "卖": "賣",
|
|
71
|
+
"货": "貨", "币": "幣", "债": "債", "万": "萬", "亿": "億", "广": "廣",
|
|
72
|
+
"宁": "寧", "辽": "遼", "苏": "蘇", "无": "無", "与": "與", "为": "為",
|
|
73
|
+
"会": "會", "员": "員", "计": "計", "划": "劃", "实": "實", "现": "現",
|
|
74
|
+
"场": "場", "区": "區", "儿": "兒", "妇": "婦", "学": "學", "术": "術",
|
|
75
|
+
"艺": "藝", "乐": "樂", "闻": "聞", "题": "題", "问": "問", "见": "見",
|
|
76
|
+
"观": "觀", "识": "識", "说": "說", "请": "請", "谢": "謝", "让": "讓",
|
|
77
|
+
"对": "對", "应": "應", "该": "該", "个": "個", "这": "這", "时": "時",
|
|
78
|
+
"间": "間", "户": "戶", "决": "決", "议": "議", "注": "註",
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
# 常见公告主题词(简体→繁体)。字表之外的整词映射,覆盖「回购/业绩」这类
|
|
82
|
+
# 高频查询;未覆盖的简体生僻词仍可用代码查询命中。
|
|
83
|
+
_HKEX_TERMS = {
|
|
84
|
+
"回购": "購回", "回購": "購回", "回购注销": "購回註銷",
|
|
85
|
+
"业绩": "業績", "年报": "年報", "月报表": "月報表", "中期报告": "中期報告",
|
|
86
|
+
"分红": "分紅", "股东": "股東", "股东大会": "股東大會",
|
|
87
|
+
"董事会": "董事會", "收购": "收購", "合并": "合併",
|
|
88
|
+
"私有化": "私有化", "供股": "供股", "配股": "配股",
|
|
89
|
+
"停牌": "停牌", "复牌": "復牌", "招股": "招股",
|
|
90
|
+
"股权": "股權", "债券": "債券", "增持": "增持", "减持": "減持",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# 查询里描述「要什么」而非「哪家公司」的词,解析公司名前先剥掉。
|
|
94
|
+
# 主题词(回购/业绩/停牌…)不在此列——它们是过滤条件,剥掉就退化成
|
|
95
|
+
# 「该公司最新公告」,用户要的特定公告会被淹掉。
|
|
96
|
+
_HKEX_NOISE = re.compile(
|
|
97
|
+
r"(?i)(公告|通告|披露|文件|最新|查询|查詢|全部|港股|港交所|披露易|hkex|H股|红筹|"
|
|
98
|
+
r"恒生指数|announcements?|filing|filings?)"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _hkex_s2t(text: str) -> str:
|
|
103
|
+
out = str(text or "")
|
|
104
|
+
# 最长词优先替换:防止「回购」先把「回购注销」前半截换掉而漏掉「注销→註銷」
|
|
105
|
+
for s, t in sorted(_HKEX_TERMS.items(), key=lambda kv: len(kv[0]), reverse=True):
|
|
106
|
+
out = out.replace(s, t)
|
|
107
|
+
return out.translate(_HKEX_S2T_CHARS)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_HKEX_PREFIX_CACHE: dict[str, list[dict[str, Any]]] = {}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _hkex_prefix(probe: str) -> list[dict[str, Any]]:
|
|
114
|
+
hit = _HKEX_PREFIX_CACHE.get(probe)
|
|
115
|
+
if hit is not None:
|
|
116
|
+
return hit
|
|
117
|
+
url = ("https://www1.hkexnews.hk/search/prefix.do?"
|
|
118
|
+
+ urllib.parse.urlencode({"callback": "callback", "lang": "ZH",
|
|
119
|
+
"type": "A", "name": probe,
|
|
120
|
+
"market": "SEHK"}))
|
|
121
|
+
req = urllib.request.Request(url, headers={
|
|
122
|
+
"User-Agent": _UA,
|
|
123
|
+
"Referer": "https://www1.hkexnews.hk/search/titlesearch.xhtml",
|
|
124
|
+
})
|
|
125
|
+
with http_open(req, timeout=10, engine="hkex") as resp:
|
|
126
|
+
body = resp.read().decode("utf-8", "replace")
|
|
127
|
+
m = re.search(r"callback\((.*)\)\s*;?\s*$", body, re.S)
|
|
128
|
+
info: list[dict[str, Any]] = []
|
|
129
|
+
if m:
|
|
130
|
+
try:
|
|
131
|
+
info = (json.loads(m.group(1)) or {}).get("stockInfo") or []
|
|
132
|
+
except (json.JSONDecodeError, ValueError):
|
|
133
|
+
info = []
|
|
134
|
+
if len(_HKEX_PREFIX_CACHE) > 200:
|
|
135
|
+
_HKEX_PREFIX_CACHE.clear()
|
|
136
|
+
_HKEX_PREFIX_CACHE[probe] = info
|
|
137
|
+
return info
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_HKEX_NON_EQUITY = re.compile(r"(購|沽|牛|熊|證|ETF|基金|債券|票據|優先|票據)")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
144
|
+
|
|
145
|
+
_TOPIC_TERMS = tuple(sorted(
|
|
146
|
+
frozenset(_HKEX_TERMS) | frozenset(_HKEX_TERMS.values()),
|
|
147
|
+
key=len, reverse=True,
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _hkex_resolve(query: str) -> tuple[list[tuple[str, str, str]], str] | None:
|
|
152
|
+
"""查询 → ([(stockId, code, name)…], core)。
|
|
153
|
+
|
|
154
|
+
code 优先;公司名先剥主题词(「腾讯 回购」→「騰訊」)再做简→繁,然后按
|
|
155
|
+
「原名 → 加控股/股份/集團 → 截短」探测,前 6 个候选并发发(单次 2~3s,
|
|
156
|
+
串行会把整次查询顶到 10s 预算外)。
|
|
157
|
+
|
|
158
|
+
加后缀是必需的:前缀接口对两字简称只回窝轮(实测「騰訊」前 6 条全是
|
|
159
|
+
「騰訊中銀…購」),「騰訊控股」才回正股 00700。返回的 core 是命中的
|
|
160
|
+
那截,调用方据此把主题词从查询里扣出来。
|
|
161
|
+
"""
|
|
162
|
+
q = (query or "").strip()
|
|
163
|
+
if not q:
|
|
164
|
+
return None
|
|
165
|
+
m = re.search(r"(?<!\d)(\d{5})(?!\d)", q)
|
|
166
|
+
if not m:
|
|
167
|
+
m4 = re.search(r"(?<!\d)(\d{4})(?!\d)", q)
|
|
168
|
+
code = m4.group(1).zfill(5) if m4 else ""
|
|
169
|
+
else:
|
|
170
|
+
code = m.group(1)
|
|
171
|
+
if code:
|
|
172
|
+
rows = [(str(i.get("stockId") or "").strip(), str(i.get("code") or ""),
|
|
173
|
+
str(i.get("name") or "")) for i in _hkex_prefix(code)]
|
|
174
|
+
rows = [r for r in rows if r[0]]
|
|
175
|
+
return (rows[:2], code) if rows else None
|
|
176
|
+
|
|
177
|
+
raw = _HKEX_NOISE.sub(" ", q)
|
|
178
|
+
raw = re.sub(r"\s+", " ", raw).strip()
|
|
179
|
+
full = _hkex_s2t(raw)
|
|
180
|
+
if not full:
|
|
181
|
+
return None
|
|
182
|
+
name_only = full
|
|
183
|
+
for term in _TOPIC_TERMS:
|
|
184
|
+
if term and term in name_only:
|
|
185
|
+
name_only = name_only.replace(term, " ")
|
|
186
|
+
name_only = re.sub(r"\s+", " ", name_only).strip() or full
|
|
187
|
+
|
|
188
|
+
candidates: list[tuple[str, str]] = [] # (probe, core)
|
|
189
|
+
|
|
190
|
+
def _add(probe: str, core: str) -> None:
|
|
191
|
+
probe = probe.strip()
|
|
192
|
+
if probe and probe not in [c[0] for c in candidates]:
|
|
193
|
+
candidates.append((probe, core))
|
|
194
|
+
|
|
195
|
+
for base in (name_only, full):
|
|
196
|
+
for suffix in ("控股", "股份", "集團", "集团", ""):
|
|
197
|
+
_add(base + suffix, base)
|
|
198
|
+
parts = name_only.split()
|
|
199
|
+
for i in range(len(parts) - 1, 0, -1):
|
|
200
|
+
_add(" ".join(parts[:i]), " ".join(parts[:i]))
|
|
201
|
+
for size in (4, 3, 2):
|
|
202
|
+
if len(name_only) > size:
|
|
203
|
+
_add(name_only[:size], name_only[:size])
|
|
204
|
+
candidates = candidates[:4]
|
|
205
|
+
if not candidates:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def _probe(item: tuple[str, str]) -> list[tuple[str, str, str]]:
|
|
209
|
+
probe, _core = item
|
|
210
|
+
info = _hkex_prefix(probe)
|
|
211
|
+
rows = [(str(i.get("stockId") or "").strip(), str(i.get("code") or ""),
|
|
212
|
+
str(i.get("name") or "")) for i in info
|
|
213
|
+
if not _HKEX_NON_EQUITY.search(str(i.get("name") or ""))]
|
|
214
|
+
return [r for r in rows if r[0]]
|
|
215
|
+
|
|
216
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
217
|
+
results = list(pool.map(_probe, candidates))
|
|
218
|
+
for (probe, core), rows in zip(candidates, results):
|
|
219
|
+
if rows:
|
|
220
|
+
rows.sort(key=lambda r: abs(len(r[2]) - len(core)))
|
|
221
|
+
return rows[:2], core
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _hkex_query(params: dict[str, Any], kw_tokens: list[str], filt: bool,
|
|
226
|
+
to: float, n: int, seen: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
227
|
+
"""按一组参数拉公告并转成统一结果;filt=True 时按主题词过滤标题。"""
|
|
228
|
+
url = ("https://www1.hkexnews.hk/search/titleSearchServlet.do?"
|
|
229
|
+
+ urllib.parse.urlencode(params))
|
|
230
|
+
req = urllib.request.Request(url, headers={
|
|
231
|
+
"User-Agent": _UA,
|
|
232
|
+
"Referer": "https://www1.hkexnews.hk/search/titlesearch.xhtml",
|
|
233
|
+
})
|
|
234
|
+
with http_open(req, timeout=to, engine="hkex") as resp:
|
|
235
|
+
payload = json.loads(resp.read().decode("utf-8", "replace"))
|
|
236
|
+
raw = payload.get("result") or "[]"
|
|
237
|
+
rows = json.loads(raw) if isinstance(raw, str) else raw
|
|
238
|
+
if not isinstance(rows, list):
|
|
239
|
+
return []
|
|
240
|
+
have = {str(r.get("url") or "") for r in seen}
|
|
241
|
+
out: list[dict[str, Any]] = []
|
|
242
|
+
for row in rows:
|
|
243
|
+
if not isinstance(row, dict):
|
|
244
|
+
continue
|
|
245
|
+
title = re.sub(r"<[^>]+>", "", str(row.get("TITLE") or "")).strip()
|
|
246
|
+
long_text = re.sub(r"<[^>]+>", "", str(row.get("LONG_TEXT") or "")).strip()
|
|
247
|
+
body = long_text or title
|
|
248
|
+
if filt and kw_tokens and not all(t in body for t in kw_tokens):
|
|
249
|
+
continue
|
|
250
|
+
code = re.sub(r"<[^>]+>", " ", str(row.get("STOCK_CODE") or "")).split()
|
|
251
|
+
name = re.sub(r"<[^>]+>", " ", str(row.get("STOCK_NAME") or "")).split()
|
|
252
|
+
link = str(row.get("FILE_LINK") or "").strip()
|
|
253
|
+
if not link:
|
|
254
|
+
continue
|
|
255
|
+
full = "https://www1.hkexnews.hk" + link
|
|
256
|
+
if full in have:
|
|
257
|
+
continue
|
|
258
|
+
have.add(full)
|
|
259
|
+
code_s = code[0] if code else ""
|
|
260
|
+
name_s = name[0] if name else ""
|
|
261
|
+
when = str(row.get("DATE_TIME") or "").strip()
|
|
262
|
+
published = ""
|
|
263
|
+
m = re.match(r"(\d{2})/(\d{2})/(\d{4})\s+(\d{2}):(\d{2})", when)
|
|
264
|
+
if m:
|
|
265
|
+
d, mo, y, hh, mm = m.groups()
|
|
266
|
+
published = f"{y}-{mo}-{d}T{hh}:{mm}:00+08:00"
|
|
267
|
+
out.append({
|
|
268
|
+
"title": f"{code_s} {name_s}|{body}"[:160],
|
|
269
|
+
"url": full,
|
|
270
|
+
"snippet": (f"{body} · {when} · {row.get('FILE_TYPE') or ''} "
|
|
271
|
+
f"{row.get('FILE_INFO') or ''}").strip(" ·"),
|
|
272
|
+
"source": "hkex",
|
|
273
|
+
"published_at": published,
|
|
274
|
+
"facts": {"stock_code": code_s, "stock_name": name_s,
|
|
275
|
+
"news_id": str(row.get("NEWS_ID") or "")},
|
|
276
|
+
})
|
|
277
|
+
if len(seen) + len(out) >= max(1, n):
|
|
278
|
+
break
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _build_hkex_engine(spec: dict[str, Any]) -> Any:
|
|
283
|
+
timeout = spec.get("timeout", 20)
|
|
284
|
+
|
|
285
|
+
@safe_search
|
|
286
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
287
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
288
|
+
to = _timeout or timeout
|
|
289
|
+
q = (query or "").strip()
|
|
290
|
+
if not q:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
hit = None
|
|
294
|
+
try:
|
|
295
|
+
hit = _hkex_resolve(q)
|
|
296
|
+
except Exception:
|
|
297
|
+
hit = None
|
|
298
|
+
|
|
299
|
+
# 剩余主题词:公司解析命中时扣掉 probe,否则用整条查询
|
|
300
|
+
base = _hkex_s2t(_HKEX_NOISE.sub(" ", q))
|
|
301
|
+
base = re.sub(r"(?<!\d)\d{4,5}(?!\d)", " ", base)
|
|
302
|
+
base = re.sub(r"\s+", " ", base).strip()
|
|
303
|
+
topic = base
|
|
304
|
+
if hit and hit[1] and hit[1] in base:
|
|
305
|
+
topic = base.replace(hit[1], " ", 1).strip()
|
|
306
|
+
topic = re.sub(r"\s+", " ", topic).strip()
|
|
307
|
+
|
|
308
|
+
has_cjk = bool(re.search(r"[\u4e00-\u9fff]", topic))
|
|
309
|
+
targets: list[tuple[str, str, str]] = []
|
|
310
|
+
if hit:
|
|
311
|
+
targets = hit[0]
|
|
312
|
+
elif topic and has_cjk:
|
|
313
|
+
targets = [("-1", "", "")]
|
|
314
|
+
else:
|
|
315
|
+
# 既没有可解析的标的、也没有中文主题词:不猜,交回上游路由
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
kw_tokens = [t for t in re.split(r"\s+", topic) if t] if (has_cjk and hit) else []
|
|
319
|
+
# 公司+主题时只查最匹配的一家(前缀接口对简称会返回多家关联主体,
|
|
320
|
+
# 逐家拉 60 条会把单次查询推到 10s 预算之上);纯代码/纯公司名查 2 家。
|
|
321
|
+
targets = targets[:1] if kw_tokens else targets[:2]
|
|
322
|
+
out: list[dict[str, Any]] = []
|
|
323
|
+
for sid, _code, _name in targets:
|
|
324
|
+
if len(out) >= max(1, n):
|
|
325
|
+
break
|
|
326
|
+
params: dict[str, Any] = {
|
|
327
|
+
"sortDir": "0", "sortByOptions": "DateTime", "category": "0",
|
|
328
|
+
"market": "SEHK", "documentType": "-1",
|
|
329
|
+
# 公司+主题词查的是「历史上哪份公告讲了这个」,45 天窗口
|
|
330
|
+
# 会漏掉半年报/年报(业绩类公告的间隔本来就长),放宽到 180 天。
|
|
331
|
+
"fromDate": (_today_cst() - timedelta(
|
|
332
|
+
days=180 if kw_tokens else 45)).strftime("%Y%m%d"),
|
|
333
|
+
"toDate": _today_cst().strftime("%Y%m%d"),
|
|
334
|
+
"title": "", "searchType": "0",
|
|
335
|
+
"t1code": "-2", "t2Gcode": "-2", "t2code": "-2",
|
|
336
|
+
"rowRange": str(max(5, min(int(n) * 3, 30))), "lang": "ZH",
|
|
337
|
+
}
|
|
338
|
+
if sid == "-1":
|
|
339
|
+
params["stockId"] = "-1"
|
|
340
|
+
params["title"] = topic
|
|
341
|
+
params["searchType"] = "1"
|
|
342
|
+
else:
|
|
343
|
+
params["stockId"] = sid
|
|
344
|
+
if kw_tokens:
|
|
345
|
+
params["rowRange"] = "60" # 公司+主题:多拉再本地筛
|
|
346
|
+
out.extend(_hkex_query(params, kw_tokens, filt=bool(hit), to=to,
|
|
347
|
+
n=n, seen=out))
|
|
348
|
+
return out[:max(1, n)]
|
|
349
|
+
|
|
350
|
+
return _engine
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ── 上海期货交易所日行情 ─────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
_SHFE_CACHE: dict[str, tuple[float, dict[str, Any]]] = {}
|
|
356
|
+
_SHFE_TTL = 600.0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _shfe_daily(engine: str, timeout: float) -> tuple[str, dict[str, Any]] | None:
|
|
360
|
+
"""取最近一个可用交易日的全量日行情(周末/节假日自动往前找)。"""
|
|
361
|
+
today = _today_cst()
|
|
362
|
+
for back in range(0, 12):
|
|
363
|
+
day = today - timedelta(days=back)
|
|
364
|
+
if day.weekday() >= 5:
|
|
365
|
+
continue
|
|
366
|
+
key = day.strftime("%Y%m%d")
|
|
367
|
+
hit = _SHFE_CACHE.get(key)
|
|
368
|
+
if hit and time.time() - hit[0] < _SHFE_TTL:
|
|
369
|
+
return key, hit[1]
|
|
370
|
+
url = ("https://www.shfe.com.cn/data/tradedata/future/dailydata/"
|
|
371
|
+
f"kx{key}.dat")
|
|
372
|
+
try:
|
|
373
|
+
data = _json_open(url, timeout, engine)
|
|
374
|
+
except Exception:
|
|
375
|
+
continue
|
|
376
|
+
rows = (data or {}).get("o_curinstrument") if isinstance(data, dict) else None
|
|
377
|
+
# 当日收盘前文件已生成但价格为空的快照(实测 2026-10-21 14:00 的
|
|
378
|
+
# kx20261021.dat:331 行、CLOSEPRICE 全空)不能当结果用,继续往前找。
|
|
379
|
+
if not rows or not any(str(r.get("CLOSEPRICE") or "").strip() for r in rows
|
|
380
|
+
if isinstance(r, dict)):
|
|
381
|
+
continue
|
|
382
|
+
_SHFE_CACHE[key] = (time.time(), data)
|
|
383
|
+
if len(_SHFE_CACHE) > 8:
|
|
384
|
+
oldest = min(_SHFE_CACHE, key=lambda k: _SHFE_CACHE[k][0])
|
|
385
|
+
_SHFE_CACHE.pop(oldest, None)
|
|
386
|
+
return key, data
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _fmt_num(v: Any) -> str:
|
|
391
|
+
try:
|
|
392
|
+
f = float(v)
|
|
393
|
+
except (TypeError, ValueError):
|
|
394
|
+
return "-"
|
|
395
|
+
if f == int(f):
|
|
396
|
+
return f"{int(f):,}"
|
|
397
|
+
return f"{f:,.2f}"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _build_shfe_engine(spec: dict[str, Any]) -> Any:
|
|
401
|
+
timeout = spec.get("timeout", 20)
|
|
402
|
+
|
|
403
|
+
@safe_search
|
|
404
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
405
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
406
|
+
to = _timeout or timeout
|
|
407
|
+
q = (query or "").strip()
|
|
408
|
+
if not q:
|
|
409
|
+
return []
|
|
410
|
+
# 「沪铜主力」「螺纹钢 2610」这类查询:剥掉交易所/意图噪声,留品种名
|
|
411
|
+
core = re.sub(
|
|
412
|
+
r"(?i)(上期所|上海期货交易所|期货|主力|连续|合约|行情|价格|报价|今日|最新|"
|
|
413
|
+
r"收盘|结算价|shfe|futures?)", " ", q)
|
|
414
|
+
core = core.replace("沪", " ")
|
|
415
|
+
core = re.sub(r"(?<!\d)\d{4}(?!\d)", " ", core)
|
|
416
|
+
core = re.sub(r"\s+", " ", core).strip().lower()
|
|
417
|
+
if not core:
|
|
418
|
+
return []
|
|
419
|
+
got = _shfe_daily("shfe", to)
|
|
420
|
+
if not got:
|
|
421
|
+
return []
|
|
422
|
+
day, data = got
|
|
423
|
+
rows = data.get("o_curinstrument") or []
|
|
424
|
+
out: list[dict[str, Any]] = []
|
|
425
|
+
for row in rows:
|
|
426
|
+
if not isinstance(row, dict):
|
|
427
|
+
continue
|
|
428
|
+
pname = str(row.get("PRODUCTNAME") or "").strip()
|
|
429
|
+
pgroup = str(row.get("PRODUCTGROUPID") or "").strip().lower()
|
|
430
|
+
pid = str(row.get("PRODUCTID") or "").strip().lower()
|
|
431
|
+
if not pname:
|
|
432
|
+
continue
|
|
433
|
+
hay = f"{pname} {pgroup} {pid}".lower()
|
|
434
|
+
if core not in hay and not any(tok in hay for tok in core.split()):
|
|
435
|
+
continue
|
|
436
|
+
close = _fmt_num(row.get("CLOSEPRICE"))
|
|
437
|
+
settle = _fmt_num(row.get("SETTLEMENTPRICE"))
|
|
438
|
+
chg = _fmt_num(row.get("ZD1_CHG"))
|
|
439
|
+
month = str(row.get("DELIVERYMONTH") or "").strip()
|
|
440
|
+
out.append({
|
|
441
|
+
"title": f"{pname} {month} 收 {close} 结算 {settle}({day})",
|
|
442
|
+
"url": f"https://www.shfe.com.cn/products/future/{pgroup}/",
|
|
443
|
+
"snippet": (f"上海期货交易所日行情 · {pname} {month} · "
|
|
444
|
+
f"收盘 {close} · 结算 {settle} · 涨跌 {chg} · "
|
|
445
|
+
f"成交 {_fmt_num(row.get('VOLUME'))} 手 · "
|
|
446
|
+
f"持仓 {_fmt_num(row.get('OPENINTEREST'))} 手"),
|
|
447
|
+
"source": "shfe",
|
|
448
|
+
"published_at": f"{day[:4]}-{day[4:6]}-{day[6:]}T15:00:00+08:00",
|
|
449
|
+
"facts": {"product": pname, "month": month, "date": day,
|
|
450
|
+
"close": close, "settlement": settle},
|
|
451
|
+
})
|
|
452
|
+
if len(out) >= max(1, n):
|
|
453
|
+
break
|
|
454
|
+
return out
|
|
455
|
+
|
|
456
|
+
return _engine
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ── 中国外汇交易中心人民币中间价 ─────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
# 币种词 → 披露易口径的货币对(100JPY 表示每 100 日元报价,是 CFETS 惯例)
|
|
462
|
+
_CFETS_CCY = {
|
|
463
|
+
"美元": "USD/CNY", "美金": "USD/CNY", "usd": "USD/CNY",
|
|
464
|
+
"欧元": "EUR/CNY", "歐元": "EUR/CNY", "eur": "EUR/CNY",
|
|
465
|
+
"日元": "100JPY/CNY", "日币": "100JPY/CNY", "jpy": "100JPY/CNY",
|
|
466
|
+
"港元": "HKD/CNY", "港币": "HKD/CNY", "港幣": "HKD/CNY", "hkd": "HKD/CNY",
|
|
467
|
+
"英镑": "GBP/CNY", "英鎊": "GBP/CNY", "gbp": "GBP/CNY",
|
|
468
|
+
"澳元": "AUD/CNY", "aud": "AUD/CNY",
|
|
469
|
+
"新西兰元": "NZD/CNY", "nzd": "NZD/CNY",
|
|
470
|
+
"新加坡元": "SGD/CNY", "sgd": "SGD/CNY",
|
|
471
|
+
"瑞士法郎": "CHF/CNY", "chf": "CHF/CNY",
|
|
472
|
+
"加元": "CAD/CNY", "加拿大元": "CAD/CNY", "cad": "CAD/CNY",
|
|
473
|
+
"澳门元": "CNY/MOP", "mop": "CNY/MOP",
|
|
474
|
+
"林吉特": "CNY/MYR", "myr": "CNY/MYR",
|
|
475
|
+
"卢布": "CNY/RUB", "rub": "CNY/RUB",
|
|
476
|
+
"兰特": "CNY/ZAR", "zar": "CNY/ZAR",
|
|
477
|
+
"韩元": "CNY/KRW", "韩币": "CNY/KRW", "krw": "CNY/KRW",
|
|
478
|
+
"迪拉姆": "CNY/AED", "aed": "CNY/AED",
|
|
479
|
+
"里亚尔": "CNY/SAR", "sar": "CNY/SAR",
|
|
480
|
+
"福林": "CNY/HUF", "huf": "CNY/HUF",
|
|
481
|
+
"兹罗提": "CNY/PLN", "pln": "CNY/PLN",
|
|
482
|
+
"丹麦克朗": "CNY/DKK", "dkk": "CNY/DKK",
|
|
483
|
+
"瑞典克朗": "CNY/SEK", "sek": "CNY/SEK",
|
|
484
|
+
"挪威克朗": "CNY/NOK", "nok": "CNY/NOK",
|
|
485
|
+
"里拉": "CNY/TRY", "try": "CNY/TRY",
|
|
486
|
+
"比索": "CNY/MXN", "mxn": "CNY/MXN",
|
|
487
|
+
"泰铢": "CNY/THB", "thb": "CNY/THB",
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _cfets_pair(query: str) -> str:
|
|
492
|
+
q = (query or "").lower()
|
|
493
|
+
for word, pair in _CFETS_CCY.items():
|
|
494
|
+
if word in q:
|
|
495
|
+
return pair
|
|
496
|
+
m = re.search(r"\b([a-z]{3})\s*[/\-]\s*(cny|rmb)\b", q)
|
|
497
|
+
if m:
|
|
498
|
+
code = m.group(1).upper()
|
|
499
|
+
for pair in _CFETS_CCY.values():
|
|
500
|
+
if pair.startswith(code + "/"):
|
|
501
|
+
return pair
|
|
502
|
+
m = re.search(r"\b(cny|rmb)\s*[/\-]\s*([a-z]{3})\b", q)
|
|
503
|
+
if m:
|
|
504
|
+
code = m.group(2).upper()
|
|
505
|
+
for pair in _CFETS_CCY.values():
|
|
506
|
+
if pair.endswith("/" + code):
|
|
507
|
+
return pair
|
|
508
|
+
return "USD/CNY" # 只说「人民币中间价」时按最常用的美元对
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _build_cfets_engine(spec: dict[str, Any]) -> Any:
|
|
512
|
+
timeout = spec.get("timeout", 15)
|
|
513
|
+
|
|
514
|
+
@safe_search
|
|
515
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
516
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
517
|
+
to = _timeout or timeout
|
|
518
|
+
if not (query or "").strip():
|
|
519
|
+
return []
|
|
520
|
+
pair = _cfets_pair(query)
|
|
521
|
+
end = _today_cst()
|
|
522
|
+
start = end - timedelta(days=14)
|
|
523
|
+
url = ("https://www.chinamoney.com.cn/ags/ms/cm-u-bk-ccpr/CcprHisNew?"
|
|
524
|
+
+ urllib.parse.urlencode({
|
|
525
|
+
"startDate": start.strftime("%Y-%m-%d"),
|
|
526
|
+
"endDate": end.strftime("%Y-%m-%d"),
|
|
527
|
+
"currency": pair,
|
|
528
|
+
"pageNum": 1,
|
|
529
|
+
"pageSize": max(1, min(int(n), 20)),
|
|
530
|
+
}))
|
|
531
|
+
data = _json_open(url, to, "cfets", headers={
|
|
532
|
+
"Referer": "https://www.chinamoney.com.cn/",
|
|
533
|
+
})
|
|
534
|
+
head = ((data or {}).get("data") or {})
|
|
535
|
+
searchlist = head.get("searchlist") or [pair]
|
|
536
|
+
out: list[dict[str, Any]] = []
|
|
537
|
+
for rec in (data or {}).get("records") or []:
|
|
538
|
+
if not isinstance(rec, dict):
|
|
539
|
+
continue
|
|
540
|
+
vals = rec.get("values") or []
|
|
541
|
+
if not vals:
|
|
542
|
+
continue
|
|
543
|
+
day = str(rec.get("date") or "")
|
|
544
|
+
value = str(vals[0]).strip()
|
|
545
|
+
label = str(searchlist[0]) if searchlist else pair
|
|
546
|
+
out.append({
|
|
547
|
+
"title": f"{label} 人民币中间价 {value}({day})",
|
|
548
|
+
"url": "https://www.chinamoney.com.cn/chinese/bkccpr/",
|
|
549
|
+
"snippet": (f"中国外汇交易中心受权公布 · {label} · {day} · "
|
|
550
|
+
f"中间价 {value}"),
|
|
551
|
+
"source": "cfets",
|
|
552
|
+
"published_at": f"{day}T09:15:00+08:00" if day else "",
|
|
553
|
+
"facts": {"pair": label, "date": day, "mid_price": value},
|
|
554
|
+
})
|
|
555
|
+
return out
|
|
556
|
+
|
|
557
|
+
return _engine
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ── DeFiLlama 协议 TVL ─────────────────────────────────────────────────────
|
|
561
|
+
|
|
562
|
+
_DLLAMA_CACHE: dict[str, Any] = {"ts": 0.0, "rows": []}
|
|
563
|
+
_DLLAMA_TTL = 300.0
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _dllama_rows(timeout: float) -> list[dict[str, Any]]:
|
|
567
|
+
now = time.time()
|
|
568
|
+
if _DLLAMA_CACHE["rows"] and now - float(_DLLAMA_CACHE["ts"]) < _DLLAMA_TTL:
|
|
569
|
+
return _DLLAMA_CACHE["rows"]
|
|
570
|
+
data = _json_open("https://api.llama.fi/protocols", max(timeout, 20), "defillama")
|
|
571
|
+
rows = data if isinstance(data, list) else []
|
|
572
|
+
_DLLAMA_CACHE["rows"] = rows
|
|
573
|
+
_DLLAMA_CACHE["ts"] = now
|
|
574
|
+
return rows
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _usd(v: Any) -> str:
|
|
578
|
+
try:
|
|
579
|
+
f = float(v)
|
|
580
|
+
except (TypeError, ValueError):
|
|
581
|
+
return "-"
|
|
582
|
+
for unit, scale in (("B", 1e9), ("M", 1e6), ("K", 1e3)):
|
|
583
|
+
if abs(f) >= scale:
|
|
584
|
+
return f"${f / scale:.2f}{unit}"
|
|
585
|
+
return f"${f:.0f}"
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _build_defillama_engine(spec: dict[str, Any]) -> Any:
|
|
589
|
+
timeout = spec.get("timeout", 30)
|
|
590
|
+
|
|
591
|
+
@safe_search
|
|
592
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
593
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
594
|
+
to = _timeout or timeout
|
|
595
|
+
q = (query or "").strip().lower()
|
|
596
|
+
if not q:
|
|
597
|
+
return []
|
|
598
|
+
noise = re.compile(
|
|
599
|
+
r"(?i)(defillama|tvl|总锁仓|liquidity|protocol|协议|链上|资金|规模|排名|"
|
|
600
|
+
r"排名前|top|多少|查询|最新)")
|
|
601
|
+
core = noise.sub(" ", q)
|
|
602
|
+
tokens = [t for t in re.split(r"\s+", core) if len(t) >= 2]
|
|
603
|
+
if not tokens:
|
|
604
|
+
return []
|
|
605
|
+
rows = _dllama_rows(to)
|
|
606
|
+
if not rows:
|
|
607
|
+
return []
|
|
608
|
+
scored: list[tuple[int, float, dict[str, Any]]] = []
|
|
609
|
+
for p in rows:
|
|
610
|
+
if not isinstance(p, dict):
|
|
611
|
+
continue
|
|
612
|
+
name = str(p.get("name") or "")
|
|
613
|
+
symbol = str(p.get("symbol") or "")
|
|
614
|
+
category = str(p.get("category") or "")
|
|
615
|
+
chains = p.get("chains") or []
|
|
616
|
+
hay = f"{name} {symbol} {category} {' '.join(map(str, chains[:8]))}".lower()
|
|
617
|
+
score = sum(1 for t in tokens if t in hay)
|
|
618
|
+
if score == 0:
|
|
619
|
+
continue
|
|
620
|
+
try:
|
|
621
|
+
tvl = float(p.get("tvl") or 0)
|
|
622
|
+
except (TypeError, ValueError):
|
|
623
|
+
tvl = 0.0
|
|
624
|
+
scored.append((score, tvl, p))
|
|
625
|
+
scored.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
|
626
|
+
out: list[dict[str, Any]] = []
|
|
627
|
+
for score, tvl, p in scored[:max(1, n)]:
|
|
628
|
+
name = str(p.get("name") or "")
|
|
629
|
+
symbol = str(p.get("symbol") or "")
|
|
630
|
+
slug = str(p.get("slug") or "")
|
|
631
|
+
cat = str(p.get("category") or "")
|
|
632
|
+
chg1 = p.get("change_1d")
|
|
633
|
+
chg7 = p.get("change_7d")
|
|
634
|
+
chg_txt = ""
|
|
635
|
+
try:
|
|
636
|
+
chg_txt = f"24h {float(chg1):+.2f}%"
|
|
637
|
+
except (TypeError, ValueError):
|
|
638
|
+
pass
|
|
639
|
+
try:
|
|
640
|
+
chg_txt += f" · 7d {float(chg7):+.2f}%"
|
|
641
|
+
except (TypeError, ValueError):
|
|
642
|
+
pass
|
|
643
|
+
desc = re.sub(r"\s+", " ", str(p.get("description") or "")).strip()
|
|
644
|
+
out.append({
|
|
645
|
+
"title": f"{name}({symbol or '-'})TVL {_usd(tvl)}"
|
|
646
|
+
+ (f" · {cat}" if cat else ""),
|
|
647
|
+
"url": (f"https://defillama.com/protocol/{slug}" if slug
|
|
648
|
+
else str(p.get("url") or "https://defillama.com/protocols")),
|
|
649
|
+
"snippet": " · ".join(x for x in (chg_txt, desc[:180]) if x),
|
|
650
|
+
"source": "defillama",
|
|
651
|
+
"facts": {"protocol": name, "tvl_usd": tvl,
|
|
652
|
+
"change_1d": chg1, "change_7d": chg7},
|
|
653
|
+
})
|
|
654
|
+
return out
|
|
655
|
+
|
|
656
|
+
return _engine
|