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
package/scripts/tfidf_router.py
CHANGED
|
@@ -25,6 +25,8 @@ from collections import Counter
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from typing import Optional
|
|
27
27
|
|
|
28
|
+
import argo_paths as _paths
|
|
29
|
+
|
|
28
30
|
# ── 路径 ──────────────────────────────────────────────────────────────────────
|
|
29
31
|
|
|
30
32
|
SKILL_DIR = Path(__file__).parent.parent
|
|
@@ -33,18 +35,25 @@ DOMAIN_PROFILES_PATH = BACKENDS_DIR / "domain_profiles.json"
|
|
|
33
35
|
|
|
34
36
|
# ── 分词 ──────────────────────────────────────────────────────────────────────
|
|
35
37
|
|
|
36
|
-
_TOKEN_PATTERN = re.compile(r"[
|
|
38
|
+
_TOKEN_PATTERN = re.compile(r"[一-鿿\u3040-\u30ff\uac00-\ud7af]|[a-zA-Z0-9]+")
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def tokenize(text: str) -> list[str]:
|
|
40
|
-
"""
|
|
42
|
+
"""简单分词:中文/日文/韩文单字 + 英文单词,统一小写。
|
|
43
|
+
|
|
44
|
+
扩展 v2.8:并入假名(\\u3040-\\u30ff)与谚文(\\uac00-\\ud7af),否则韩/日
|
|
45
|
+
查询 tokenize 恒空、TF-IDF 语义路由对多语言整体失明。单字拉丁仍丢弃(词袋无关)。
|
|
46
|
+
"""
|
|
41
47
|
tokens = []
|
|
42
48
|
for match in _TOKEN_PATTERN.finditer(text.lower()):
|
|
43
49
|
token = match.group()
|
|
44
|
-
if len(token)
|
|
45
|
-
tokens.append(token)
|
|
46
|
-
elif len(token) > 1:
|
|
50
|
+
if len(token) > 1:
|
|
47
51
|
tokens.append(token)
|
|
52
|
+
elif len(token) == 1:
|
|
53
|
+
ch = token[0]
|
|
54
|
+
if ('一' <= ch <= '鿿' or '\u3040' <= ch <= '\u30ff'
|
|
55
|
+
or '\uac00' <= ch <= '\ud7af'):
|
|
56
|
+
tokens.append(ch)
|
|
48
57
|
return tokens
|
|
49
58
|
|
|
50
59
|
|
|
@@ -100,43 +109,87 @@ def cosine_similarity(vec_a: dict[str, float], vec_b: dict[str, float]) -> float
|
|
|
100
109
|
return dot / (norm_a * norm_b)
|
|
101
110
|
|
|
102
111
|
|
|
103
|
-
# ──
|
|
112
|
+
# ── 配额 / 成本(mtime 缓存,避免 route 每次为每个引擎读盘) ──────────────────
|
|
113
|
+
|
|
114
|
+
_quota_state_cache: dict | None = None
|
|
115
|
+
_quota_state_mtime: float = -1.0
|
|
116
|
+
_cost_profiles_cache: dict | None = None
|
|
117
|
+
_cost_profiles_mtime: float = -1.0
|
|
118
|
+
|
|
104
119
|
|
|
105
120
|
def _load_quota_state() -> dict:
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
"""加载配额状态文件(mtime 缓存)。"""
|
|
122
|
+
global _quota_state_cache, _quota_state_mtime
|
|
123
|
+
quota_path = _paths.state_path("quota.json")
|
|
124
|
+
try:
|
|
125
|
+
mtime = quota_path.stat().st_mtime
|
|
126
|
+
except OSError:
|
|
109
127
|
return {}
|
|
128
|
+
if _quota_state_cache is not None and mtime == _quota_state_mtime:
|
|
129
|
+
return _quota_state_cache
|
|
110
130
|
try:
|
|
111
|
-
|
|
131
|
+
data = json.loads(quota_path.read_bytes())
|
|
112
132
|
except (json.JSONDecodeError, OSError):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
data = {}
|
|
134
|
+
_quota_state_cache = data if isinstance(data, dict) else {}
|
|
135
|
+
_quota_state_mtime = mtime
|
|
136
|
+
return _quota_state_cache
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _quota_ratio(engine: str, quota_state: dict,
|
|
140
|
+
profiles: dict | None = None) -> float:
|
|
141
|
+
"""计算配额剩余比例。返回 0.0-1.0,无记录时默认 1.0。
|
|
142
|
+
|
|
143
|
+
limit 的唯一来源是 quota_profiles.json(声明文件);配额状态文件里只有
|
|
144
|
+
用量计数。此前本函数只读 state["limit"],而写入方(quota.record)恒填 0,
|
|
145
|
+
于是 limit<=0 走「无限配额」分支恒返回 1.0——配额感知惩罚(<0.2 砍到
|
|
146
|
+
2×qr、<0.5 乘 0.5+qr)从来没生效过。与 quota.get_remaining_ratio 的
|
|
147
|
+
profile.get("limit") 计算方式保持一致,缺省才回落到 state 里的值。
|
|
148
|
+
"""
|
|
149
|
+
info = quota_state.get(engine) or {}
|
|
150
|
+
profile = (profiles if profiles is not None else _load_cost_profiles()).get(engine) or {}
|
|
151
|
+
limit = profile.get("limit")
|
|
152
|
+
if not isinstance(limit, (int, float)) or limit <= 0:
|
|
153
|
+
limit = info.get("limit")
|
|
154
|
+
if limit is None or limit <= 0:
|
|
155
|
+
# 无限配额 / 脏数据(limit=0)不当作耗尽
|
|
120
156
|
return 1.0
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return max(0, limit - used) / limit
|
|
157
|
+
used = info.get("used", 0) or 0
|
|
158
|
+
return max(0.0, float(limit - used) / float(limit))
|
|
124
159
|
|
|
125
160
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _cost_factor(engine: str) -> float:
|
|
129
|
-
"""获取引擎成本因子:free=1.0, low=0.85, paid=0.6。"""
|
|
161
|
+
def _load_cost_profiles() -> dict:
|
|
162
|
+
global _cost_profiles_cache, _cost_profiles_mtime
|
|
130
163
|
tiers_path = BACKENDS_DIR / "quota_profiles.json"
|
|
131
|
-
if not tiers_path.exists():
|
|
132
|
-
return 1.0
|
|
133
164
|
try:
|
|
134
|
-
|
|
165
|
+
mtime = tiers_path.stat().st_mtime
|
|
166
|
+
except OSError:
|
|
167
|
+
return {}
|
|
168
|
+
if _cost_profiles_cache is not None and mtime == _cost_profiles_mtime:
|
|
169
|
+
return _cost_profiles_cache
|
|
170
|
+
try:
|
|
171
|
+
data = json.loads(tiers_path.read_bytes())
|
|
135
172
|
except (json.JSONDecodeError, OSError):
|
|
136
|
-
|
|
173
|
+
data = {}
|
|
174
|
+
_cost_profiles_cache = data if isinstance(data, dict) else {}
|
|
175
|
+
_cost_profiles_mtime = mtime
|
|
176
|
+
return _cost_profiles_cache
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _cost_factor(engine: str, profiles: dict | None = None) -> float:
|
|
180
|
+
"""获取引擎成本因子:free=1.0, low=0.85, paid=0.6。"""
|
|
181
|
+
if profiles is None:
|
|
182
|
+
profiles = _load_cost_profiles()
|
|
137
183
|
profile = profiles.get(engine, {})
|
|
184
|
+
if not isinstance(profile, dict):
|
|
185
|
+
return 1.0
|
|
138
186
|
tier = profile.get("cost_tier", "free")
|
|
139
|
-
|
|
187
|
+
# 成本因子查 config 的唯一来源(此前本模块自带一张表,与 config 的值不一致)
|
|
188
|
+
try:
|
|
189
|
+
from config import _COST_FACTOR_BY_TIER
|
|
190
|
+
return _COST_FACTOR_BY_TIER.get(tier, 1.0)
|
|
191
|
+
except Exception:
|
|
192
|
+
return 1.0
|
|
140
193
|
|
|
141
194
|
|
|
142
195
|
# ── 主路由引擎 ─────────────────────────────────────────────────────────────────
|
|
@@ -151,18 +204,34 @@ class SemanticRouter:
|
|
|
151
204
|
self.boost_keywords: dict[str, dict[str, float]] = {}
|
|
152
205
|
self.boost_combos: dict[str, dict[str, float]] = {}
|
|
153
206
|
self._loaded = False
|
|
207
|
+
self._profiles_mtime: float = 0.0
|
|
154
208
|
|
|
155
209
|
def _ensure_loaded(self) -> None:
|
|
156
|
-
"""
|
|
210
|
+
"""懒加载领域文档;文件变化时自动重载。"""
|
|
157
211
|
if self._loaded:
|
|
158
|
-
|
|
212
|
+
try:
|
|
213
|
+
mtime = DOMAIN_PROFILES_PATH.stat().st_mtime
|
|
214
|
+
if mtime == self._profiles_mtime:
|
|
215
|
+
return
|
|
216
|
+
except OSError:
|
|
217
|
+
return
|
|
218
|
+
self._loaded = False
|
|
219
|
+
self.engine_names.clear()
|
|
220
|
+
self.engine_vectors.clear()
|
|
221
|
+
self.boost_keywords.clear()
|
|
222
|
+
self.boost_combos.clear()
|
|
159
223
|
if not DOMAIN_PROFILES_PATH.exists():
|
|
160
224
|
self._loaded = True
|
|
225
|
+
# mtime 一并复位:留着旧值的话,文件以原 mtime 还原(回收站恢复/
|
|
226
|
+
# 原子写回滚)时会被上面判成「未变化」,路由带着已清空的 vectors
|
|
227
|
+
# 永久空转——静默,无任何报错。
|
|
228
|
+
self._profiles_mtime = 0.0
|
|
161
229
|
return
|
|
162
230
|
try:
|
|
163
|
-
profiles = json.loads(DOMAIN_PROFILES_PATH.
|
|
231
|
+
profiles = json.loads(DOMAIN_PROFILES_PATH.read_bytes())
|
|
164
232
|
except (json.JSONDecodeError, OSError):
|
|
165
233
|
self._loaded = True
|
|
234
|
+
self._profiles_mtime = 0.0 # 同上:坏文件不占住旧 mtime
|
|
166
235
|
return
|
|
167
236
|
|
|
168
237
|
corpus = []
|
|
@@ -178,6 +247,7 @@ class SemanticRouter:
|
|
|
178
247
|
self.vectorizer.fit(corpus)
|
|
179
248
|
for i, name in enumerate(self.engine_names):
|
|
180
249
|
self.engine_vectors[name] = self.vectorizer.transform(corpus[i])
|
|
250
|
+
self._profiles_mtime = DOMAIN_PROFILES_PATH.stat().st_mtime
|
|
181
251
|
self._loaded = True
|
|
182
252
|
|
|
183
253
|
def route(self, query: str, top_k: int = 3,
|
|
@@ -194,6 +264,8 @@ class SemanticRouter:
|
|
|
194
264
|
query_vec = self.vectorizer.transform(query)
|
|
195
265
|
scores = []
|
|
196
266
|
quota_state = _load_quota_state() if quota_aware else {}
|
|
267
|
+
cost_profiles = _load_cost_profiles()
|
|
268
|
+
q_lower = query.lower() # 一次即可,避免每个引擎重复 lower
|
|
197
269
|
|
|
198
270
|
for name in self.engine_names:
|
|
199
271
|
sim = cosine_similarity(query_vec, self.engine_vectors[name])
|
|
@@ -203,7 +275,7 @@ class SemanticRouter:
|
|
|
203
275
|
boost = 1.0
|
|
204
276
|
hit_kws = []
|
|
205
277
|
for kw, weight in self.boost_keywords.get(name, {}).items():
|
|
206
|
-
if kw.lower() in
|
|
278
|
+
if kw.lower() in q_lower:
|
|
207
279
|
boost += weight - 1.0
|
|
208
280
|
hit_kws.append(kw)
|
|
209
281
|
if hit_kws:
|
|
@@ -212,7 +284,7 @@ class SemanticRouter:
|
|
|
212
284
|
# 组合关键词加成
|
|
213
285
|
hit_combos = []
|
|
214
286
|
for combo, bonus in self.boost_combos.get(name, {}).items():
|
|
215
|
-
if all(w.lower() in
|
|
287
|
+
if all(w.lower() in q_lower for w in combo.split()):
|
|
216
288
|
boost += bonus
|
|
217
289
|
hit_combos.append(combo)
|
|
218
290
|
if hit_combos:
|
|
@@ -220,15 +292,15 @@ class SemanticRouter:
|
|
|
220
292
|
|
|
221
293
|
final_score = sim * boost
|
|
222
294
|
|
|
223
|
-
#
|
|
224
|
-
cf = _cost_factor(name)
|
|
295
|
+
# 成本感知(profiles 已缓存,不再每引擎读盘)
|
|
296
|
+
cf = _cost_factor(name, cost_profiles)
|
|
225
297
|
if cf < 1.0:
|
|
226
298
|
final_score *= cf
|
|
227
299
|
parts.append(f"cost={cf}")
|
|
228
300
|
|
|
229
301
|
# 配额感知惩罚
|
|
230
302
|
if quota_aware and quota_state:
|
|
231
|
-
qr = _quota_ratio(name, quota_state)
|
|
303
|
+
qr = _quota_ratio(name, quota_state, cost_profiles)
|
|
232
304
|
if qr < 0.2:
|
|
233
305
|
final_score *= qr * 2
|
|
234
306
|
parts.append(f"配额紧张({qr:.0%})")
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""time_utils.py — 时间窗归一化与 published_at 解析的纯函数工具箱。
|
|
3
|
+
|
|
4
|
+
从 search.py 中外提,解决 3351 行问题。本模块只含纯函数与编译好的正则,
|
|
5
|
+
零重依赖(仅 datetime + re),可被路由/搜索/抓取/研究等层安全引用而不必
|
|
6
|
+
担心循环导入或副作用。
|
|
7
|
+
|
|
8
|
+
本模块是「时间语义」的唯一来源:
|
|
9
|
+
- published_at 解析 → published_ts
|
|
10
|
+
- 时间窗归一化 → parse_time_value / normalize_time_window
|
|
11
|
+
- 结果后过滤 → apply_time_window
|
|
12
|
+
- 排序 → sort_results_by_time
|
|
13
|
+
- 哪些引擎带时间能力 → TIME_CAPABLE_ENGINES / is_time_capable
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# published_at 常见形态:YYYY-MM-DD、YYYY-MM-DD HH:MM[:SS]、ISO(YYYY-MM-DDTHH:MM:SS)
|
|
23
|
+
_DATE_RE = re.compile(
|
|
24
|
+
r"^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?"
|
|
25
|
+
)
|
|
26
|
+
# 相对时间:Nd / Nh / Nw(不区分大小写)
|
|
27
|
+
_REL_TIME_RE = re.compile(r"^(\d+)\s*([dhw])$", re.IGNORECASE)
|
|
28
|
+
# 纯日期 YYYY-MM-DD(until 边界含当天;下推保留日期形态)
|
|
29
|
+
_DATE_ONLY_RE = re.compile(r"^\d{4}-\d{1,2}-\d{1,2}$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# 带发布时间能力(published_at)的引擎集合。时间窗的缓存键隔离与后过滤
|
|
33
|
+
# 只对含这些引擎的组合生效:不带时间字段的引擎会忽略时间窗、结果相同,
|
|
34
|
+
# 隔离缓存键只会白白降低命中率(7d/30d 查同一引擎本可共享缓存)。
|
|
35
|
+
# local_search 聚合内部可能选中 news 类子引擎(带日期),保守纳入。
|
|
36
|
+
TIME_CAPABLE_ENGINES: frozenset[str] = frozenset({
|
|
37
|
+
"realtime_index", "wayback_cdx", "local_search",
|
|
38
|
+
"local_bing_news", "local_google_news", "local_duckduckgo_news",
|
|
39
|
+
"local_ddgs_news",
|
|
40
|
+
# parallel(after_date 下推)/ you(freshness 动态化 + page_age)
|
|
41
|
+
"parallel", "you",
|
|
42
|
+
# deepseek_search:上游逐条返回 RFC 3339 date(output_map 映射
|
|
43
|
+
# published_at),日期覆盖率实测 3-5/5;缺了这一条时 --since/--until
|
|
44
|
+
# 对它静默无效(time_aware=False,后过滤整段跳过),日期最全的引擎
|
|
45
|
+
# 反而用不上时间窗。
|
|
46
|
+
"deepseek_search",
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_time_capable(eng: str) -> bool:
|
|
51
|
+
"""引擎是否可能返回 published_at(决定时间窗是否参与缓存键/后过滤)。"""
|
|
52
|
+
return eng in TIME_CAPABLE_ENGINES
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def published_ts(r: dict[str, Any]) -> float | None:
|
|
56
|
+
"""解析结果的 published_at → epoch 秒;无法解析返回 None(恒排最后)。
|
|
57
|
+
|
|
58
|
+
ISO 优先(fromisoformat 支持 T/空格分隔、Z、±HH:MM 时区),
|
|
59
|
+
带时区正确换算 epoch,无时区按本地时区解释;回退 YYYY-MM-DD 手工解析。
|
|
60
|
+
"""
|
|
61
|
+
raw = r.get("published_at")
|
|
62
|
+
if not raw:
|
|
63
|
+
return None
|
|
64
|
+
text = str(raw).strip()
|
|
65
|
+
if text.isdigit(): # 部分引擎给 epoch 秒时间戳
|
|
66
|
+
try:
|
|
67
|
+
return float(text)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
dt = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
72
|
+
# aware datetime 正确换算 UTC epoch;naive 按本地时区解释
|
|
73
|
+
return dt.timestamp()
|
|
74
|
+
except ValueError:
|
|
75
|
+
pass
|
|
76
|
+
# basic ISO(YYYYMMDDTHHMMSSZ,如 GDELT seendate):Python 3.10 的
|
|
77
|
+
# fromisoformat 不认,插入分隔符归一化成扩展格式再走同一解析
|
|
78
|
+
if len(text) >= 15 and text[:8].isdigit() and text[8] == "T" and text[9:15].isdigit():
|
|
79
|
+
try:
|
|
80
|
+
dt = datetime.fromisoformat(
|
|
81
|
+
f"{text[:4]}-{text[4:6]}-{text[6:8]}"
|
|
82
|
+
f"T{text[9:11]}:{text[11:13]}:{text[13:15]}+00:00")
|
|
83
|
+
return dt.timestamp()
|
|
84
|
+
except ValueError:
|
|
85
|
+
pass
|
|
86
|
+
m = _DATE_RE.match(text)
|
|
87
|
+
if not m:
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
return datetime(
|
|
91
|
+
int(m.group(1)), int(m.group(2)), int(m.group(3)),
|
|
92
|
+
int(m.group(4) or 0), int(m.group(5) or 0), int(m.group(6) or 0),
|
|
93
|
+
).timestamp()
|
|
94
|
+
except ValueError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_time_value(value: Any) -> tuple[str | None, float | None]:
|
|
99
|
+
"""解析单边时间窗 → (归一化 ISO 字符串, epoch 秒)。
|
|
100
|
+
|
|
101
|
+
支持:相对量(Nd/Nh/Nw)、YYYY-MM-DD、YYYY-MM-DD HH:MM[:SS]、
|
|
102
|
+
ISO 8601(含 Z / ±HH:MM)、纯数字 epoch 秒。
|
|
103
|
+
相对量归一化为绝对日期(YYYY-MM-DD),语义确定、可入缓存键;
|
|
104
|
+
无法解析返回 (None, None),调用方保持原样下推、不参与后过滤。
|
|
105
|
+
"""
|
|
106
|
+
if value in (None, ""):
|
|
107
|
+
return None, None
|
|
108
|
+
text = str(value).strip()
|
|
109
|
+
# epoch 秒
|
|
110
|
+
if text.isdigit():
|
|
111
|
+
try:
|
|
112
|
+
ts = float(text)
|
|
113
|
+
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
114
|
+
return dt.isoformat(timespec="seconds"), ts
|
|
115
|
+
except (ValueError, OSError):
|
|
116
|
+
return None, None
|
|
117
|
+
# 相对量:Nd / Nh / Nw → 绝对日期(本地时区零点)
|
|
118
|
+
m = _REL_TIME_RE.match(text)
|
|
119
|
+
if m:
|
|
120
|
+
amount = int(m.group(1))
|
|
121
|
+
unit = m.group(2).lower()
|
|
122
|
+
now = datetime.now()
|
|
123
|
+
if unit == "h":
|
|
124
|
+
dt = now - timedelta(hours=amount)
|
|
125
|
+
elif unit == "w":
|
|
126
|
+
dt = now - timedelta(weeks=amount)
|
|
127
|
+
else:
|
|
128
|
+
dt = now - timedelta(days=amount)
|
|
129
|
+
d = dt.date()
|
|
130
|
+
return d.isoformat(), datetime(d.year, d.month, d.day).timestamp()
|
|
131
|
+
# 绝对时间:fromisoformat 优先(T/空格分隔、Z、±HH:MM)
|
|
132
|
+
try:
|
|
133
|
+
dt = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
134
|
+
except ValueError:
|
|
135
|
+
return None, None
|
|
136
|
+
if dt.tzinfo is not None:
|
|
137
|
+
return dt.isoformat(timespec="seconds"), dt.timestamp()
|
|
138
|
+
# 无时区:按本地时区(与 published_ts 无时区行为一致);
|
|
139
|
+
# 纯日期(时间为零点)保留 YYYY-MM-DD 形态下推,兼容引擎既有解析
|
|
140
|
+
if (dt.hour, dt.minute, dt.second, dt.microsecond) == (0, 0, 0, 0):
|
|
141
|
+
return dt.date().isoformat(), dt.timestamp()
|
|
142
|
+
return dt.isoformat(timespec="seconds"), dt.timestamp()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
#: 时间窗接受的写法(给用户看的唯一一份说明;CLI 的 argparse 与 MCP 的错误行
|
|
146
|
+
#: 都从这里取,避免「文档说支持 2w、代码其实只认 d」这类漂移)。
|
|
147
|
+
WINDOW_FORMATS_HINT = (
|
|
148
|
+
"支持 7d/24h/2w、2026-09-01、2026-09-01 12:00、"
|
|
149
|
+
"ISO 8601(含 Z / ±HH:MM)或 epoch 秒")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_valid_time_window(value: Any) -> bool:
|
|
153
|
+
"""入参能否解析成时间窗(判据与 `parse_time_value` 同源,不另写一套正则)。
|
|
154
|
+
|
|
155
|
+
why:此前非法值被**静默接受**——`--since garbage` 原样回显在响应里、
|
|
156
|
+
`time_filtered=0`、没有任何说明,调用方以为自己拿到了筛过的结果。
|
|
157
|
+
这是「静默不生效」里最坏的一种:不是少了一层增强,而是**答案被当成
|
|
158
|
+
已经筛过的**(同一条查询用 `--since 2026-09-01` 与 `--since garbage`
|
|
159
|
+
拿到同一批结果,且无从分辨)。
|
|
160
|
+
"""
|
|
161
|
+
if value in (None, ""):
|
|
162
|
+
return True # 没给就是没给,不是坏值
|
|
163
|
+
_iso, ts = parse_time_value(value)
|
|
164
|
+
return ts is not None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def normalize_time_window(
|
|
168
|
+
since: str | None, until: str | None
|
|
169
|
+
) -> tuple[str | None, str | None, float | None, float | None]:
|
|
170
|
+
"""归一化时间窗 → (since_iso, until_iso, since_ts, until_ts)。
|
|
171
|
+
|
|
172
|
+
下推与缓存键使用归一化 ISO(相对值转绝对日期,消除 7d 与绝对日期的
|
|
173
|
+
缓存碎片);后过滤使用 epoch 秒。非法输入 iso 保留原始字符串、
|
|
174
|
+
ts 为 None:仍会下推原值、缓存键仍区分,但不参与后过滤,不阻断搜索。
|
|
175
|
+
"""
|
|
176
|
+
s_iso, s_ts = parse_time_value(since)
|
|
177
|
+
u_iso, u_ts = parse_time_value(until)
|
|
178
|
+
# 纯日期 until 语义为「含当天」:边界取当天最后一刻,
|
|
179
|
+
# 使次日零点及之后的结果被剔除、当天 23:59:59 保留
|
|
180
|
+
if u_iso and _DATE_ONLY_RE.fullmatch(u_iso):
|
|
181
|
+
try:
|
|
182
|
+
d = datetime.fromisoformat(u_iso).date()
|
|
183
|
+
u_ts = datetime(d.year, d.month, d.day, 23, 59, 59, 999999).timestamp()
|
|
184
|
+
except ValueError:
|
|
185
|
+
pass
|
|
186
|
+
s_raw = str(since).strip() if since not in (None, "") else None
|
|
187
|
+
u_raw = str(until).strip() if until not in (None, "") else None
|
|
188
|
+
return (s_iso or s_raw, u_iso or u_raw, s_ts, u_ts)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def apply_time_window(
|
|
192
|
+
results: list[dict[str, Any]], since_ts: float | None, until_ts: float | None
|
|
193
|
+
) -> tuple[list[dict[str, Any]], int]:
|
|
194
|
+
"""结果后过滤保底:剔除「有 published_at 且明确超窗」的条目。
|
|
195
|
+
|
|
196
|
+
宽松策略:无时间字段的结果无法判断、予以保留(避免大多数引擎清空);
|
|
197
|
+
只有时间明确落在窗口外的才剔除。返回 (保留列表, 剔除数) 供 envelope 上报。
|
|
198
|
+
"""
|
|
199
|
+
if since_ts is None and until_ts is None:
|
|
200
|
+
return results, 0
|
|
201
|
+
kept: list[dict[str, Any]] = []
|
|
202
|
+
dropped = 0
|
|
203
|
+
for r in results:
|
|
204
|
+
ts = published_ts(r)
|
|
205
|
+
if ts is not None:
|
|
206
|
+
if since_ts is not None and ts < since_ts:
|
|
207
|
+
dropped += 1
|
|
208
|
+
continue
|
|
209
|
+
if until_ts is not None and ts > until_ts:
|
|
210
|
+
dropped += 1
|
|
211
|
+
continue
|
|
212
|
+
kept.append(r)
|
|
213
|
+
return kept, dropped
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def sort_results_by_time(results: list[dict[str, Any]], sort: str) -> list[dict[str, Any]]:
|
|
217
|
+
"""按时间重排结果集:oldest 升序 / newest 降序 / relevance 原序。
|
|
218
|
+
|
|
219
|
+
排序是纯本地展示顺序:不改变结果集、不进入缓存键、不影响缓存内容;
|
|
220
|
+
无日期条目恒排最后;同时间保持原相对顺序(稳定排序,结果可复现)。
|
|
221
|
+
"""
|
|
222
|
+
if sort not in ("oldest", "newest"):
|
|
223
|
+
return results
|
|
224
|
+
if len(results) <= 1:
|
|
225
|
+
return results
|
|
226
|
+
|
|
227
|
+
def _key(r: dict[str, Any]) -> tuple[int, float]:
|
|
228
|
+
ts = published_ts(r)
|
|
229
|
+
if ts is None:
|
|
230
|
+
return (1, 0.0) # 无日期恒排最后
|
|
231
|
+
return (0, ts if sort == "oldest" else -ts)
|
|
232
|
+
|
|
233
|
+
return sorted(results, key=_key)
|