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,136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""article.py — 微信公众号文章全文抓取。
|
|
3
|
+
|
|
4
|
+
用法: argo article <url> [--json]
|
|
5
|
+
argo article <url> --json > article.json
|
|
6
|
+
|
|
7
|
+
绕过 robots.txt 限制:公众号 robots 禁爬,但文章页对真实浏览器开放,
|
|
8
|
+
用手机 UA 直连(curl/urllib 不遵守 robots),解析 js_content 提取正文。
|
|
9
|
+
|
|
10
|
+
输出: 标题 / 作者 / 发布时间 / 正文纯文本 / 图片 URL 列表
|
|
11
|
+
"""
|
|
12
|
+
import argparse, json, re, sys, html as htmllib, urllib.request
|
|
13
|
+
from cli_io import dumps
|
|
14
|
+
|
|
15
|
+
UA = ("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
|
|
16
|
+
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1")
|
|
17
|
+
|
|
18
|
+
HEADERS = {
|
|
19
|
+
"User-Agent": UA,
|
|
20
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
21
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
22
|
+
"Referer": "https://mp.weixin.qq.com/",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fetch(url: str, timeout: int = 30) -> str:
|
|
27
|
+
req = urllib.request.Request(url, headers=HEADERS)
|
|
28
|
+
# 起点域名白名单挡不住服务端 302:重定向的每一跳都要过 SSRF 校验
|
|
29
|
+
# (http/https scheme + 非本机/内网目标),被拒即抛错而非跟随
|
|
30
|
+
opener = urllib.request.build_opener(_SafeRedirectHandler)
|
|
31
|
+
with opener.open(req, timeout=timeout) as r:
|
|
32
|
+
return r.read().decode("utf-8", errors="ignore")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _SafeRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
36
|
+
"""每跳重定向目标过 url_safety.check_url,拦截内网/非 http(s) 目标。"""
|
|
37
|
+
|
|
38
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
39
|
+
from url_safety import check_url
|
|
40
|
+
ok, reason = check_url(newurl)
|
|
41
|
+
if not ok:
|
|
42
|
+
raise ValueError(f"重定向目标被 SSRF 防护拦截({reason}): {newurl}")
|
|
43
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def strip_tags(s: str) -> str:
|
|
47
|
+
s = re.sub(r"<script[\s\S]*?</script>|<style[\s\S]*?</style>", "", s)
|
|
48
|
+
s = re.sub(r"<br\s*/?>", "\n", s)
|
|
49
|
+
s = re.sub(r"</(p|div|section|h\d|li|tr|blockquote)>", "\n", s)
|
|
50
|
+
s = re.sub(r"<[^>]+>", "", s)
|
|
51
|
+
s = htmllib.unescape(s)
|
|
52
|
+
lines = [l.strip() for l in s.split("\n")]
|
|
53
|
+
return "\n".join(l for l in lines if l)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fetch_article(url: str, timeout: int = 30) -> dict:
|
|
57
|
+
"""抓取并解析公众号文章,返回结构化 dict(CLI 与 MCP 共用主链)。
|
|
58
|
+
|
|
59
|
+
URL 非 mp.weixin.qq.com 时抛 ValueError(CLI 走 stderr 退出,
|
|
60
|
+
MCP 转 isError);抓取失败/反爬拦截返回 {"ok": False, "error": ...}。
|
|
61
|
+
"""
|
|
62
|
+
if not re.match(r"https?://mp\.weixin\.qq\.com/", url):
|
|
63
|
+
raise ValueError("仅支持 mp.weixin.qq.com 链接")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
raw = fetch(url, timeout)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return {"ok": False, "error": f"抓取失败: {e}"}
|
|
69
|
+
|
|
70
|
+
if "js_content" not in raw and "环境异常" in raw:
|
|
71
|
+
return {"ok": False, "error": "微信反爬拦截(环境异常),请稍后重试"}
|
|
72
|
+
|
|
73
|
+
def grab(pattern, default=""):
|
|
74
|
+
m = re.search(pattern, raw)
|
|
75
|
+
return m.group(1).strip() if m else default
|
|
76
|
+
|
|
77
|
+
title = grab(r'<meta property="og:title" content="([^"]+)"') or \
|
|
78
|
+
grab(r'<h1[^>]*class="rich_media_title"[^>]*>([\s\S]*?)</h1>')
|
|
79
|
+
author = grab(r'<meta property="og:article:author" content="([^"]+)"') or \
|
|
80
|
+
grab(r'id="js_name"[^>]*>([^<]+)<')
|
|
81
|
+
pubtime = grab(r'id="publish_time"[^>]*>([^<]+)<')
|
|
82
|
+
|
|
83
|
+
m = re.search(r'id="js_content"[^>]*>([\s\S]*?)</div>\s*<script', raw)
|
|
84
|
+
if not m:
|
|
85
|
+
m = re.search(r'id="js_content"[^>]*>([\s\S]*?)</div>', raw)
|
|
86
|
+
body_html = m.group(1) if m else ""
|
|
87
|
+
|
|
88
|
+
text = strip_tags(body_html)
|
|
89
|
+
imgs = re.findall(r'data-src="(https://mmbiz\.qpic\.cn/[^"]+)"', body_html)
|
|
90
|
+
if not imgs:
|
|
91
|
+
imgs = re.findall(r'src="(https://mmbiz\.qpic\.cn/[^"]+)"', body_html)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"ok": True,
|
|
95
|
+
"url": url,
|
|
96
|
+
"title": title,
|
|
97
|
+
"author": author,
|
|
98
|
+
"publish_time": pubtime,
|
|
99
|
+
"char_count": len(text),
|
|
100
|
+
"image_count": len(imgs),
|
|
101
|
+
"images": imgs,
|
|
102
|
+
"content": text,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
ap = argparse.ArgumentParser(description="微信公众号文章全文抓取")
|
|
108
|
+
ap.add_argument("url", help="mp.weixin.qq.com 文章链接")
|
|
109
|
+
ap.add_argument("--json", action="store_true", help="输出 JSON")
|
|
110
|
+
args = ap.parse_args()
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
out = fetch_article(args.url)
|
|
114
|
+
except ValueError as e:
|
|
115
|
+
print(str(e), file=sys.stderr)
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
if not out["ok"]:
|
|
118
|
+
print(dumps(out))
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
if args.json:
|
|
122
|
+
print(dumps(out))
|
|
123
|
+
else:
|
|
124
|
+
print(f"标题: {out['title']}")
|
|
125
|
+
print(f"作者: {out['author']} 时间: {out['publish_time']} "
|
|
126
|
+
f"正文 {out['char_count']} 字 / {out['image_count']} 图")
|
|
127
|
+
print("---")
|
|
128
|
+
print(out["content"])
|
|
129
|
+
if out["images"]:
|
|
130
|
+
print("---")
|
|
131
|
+
for i, u in enumerate(out["images"], 1):
|
|
132
|
+
print(f"[图{i}] {u}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""arXiv 源码包(e-print)获取与安全解包 — 论文 LaTeX 精读的取数层。
|
|
2
|
+
|
|
3
|
+
P1 论文深读三能力之一(2026-09-30 P0 实测后立项):外部论文工具的
|
|
4
|
+
get_paper_latex 实测 3.4s 拿到 main tex + 源文件清单,argo 此前无此能力。
|
|
5
|
+
|
|
6
|
+
安全边界(全部内存态,不落盘解包):
|
|
7
|
+
1. 只认 arxiv.org 的 https URL(scheme + 域名白名单,拒环回/私有地址)
|
|
8
|
+
2. 下载总量封顶(默认 64MB),成员数封顶(500),单文件解压封顶(2MB)
|
|
9
|
+
3. tar 成员名拒绝绝对路径 / 路径穿越 / 盘符——本模块只读内存,永不写文件
|
|
10
|
+
4. 只收 .tex/.bbl 文本(其余成员只列名不收内容)
|
|
11
|
+
|
|
12
|
+
缓存:走 fetch 缓存(同 id 1h 内二读零成本),与 pdf 提取缓存同一纪律。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import gzip
|
|
17
|
+
import io
|
|
18
|
+
import tarfile
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from net_proxy import open_url
|
|
22
|
+
from pdf_extract import CONTENT_WARNING, chunk_text
|
|
23
|
+
|
|
24
|
+
# 域名白名单(e-print 只在 arxiv.org 主站)
|
|
25
|
+
_ALLOWED_HOSTS = {"arxiv.org", "www.arxiv.org", "export.arxiv.org"}
|
|
26
|
+
_MAX_DOWNLOAD = 64 * 1024 * 1024 # 压缩包下载上限
|
|
27
|
+
_MAX_MEMBERS = 500 # 成员数上限
|
|
28
|
+
_MAX_TOTAL_UNCOMPRESSED = 64 * 1024 * 1024 # 解压总量上限
|
|
29
|
+
_MAX_FILE_UNCOMPRESSED = 2 * 1024 * 1024 # 单文件解压上限(.tex/.bbl)
|
|
30
|
+
_MAX_CACHE_TEXTS = 2 * 1024 * 1024 # 进缓存的文本总量上限(超出只存清单)
|
|
31
|
+
_TEX_EXTS = (".tex", ".bbl")
|
|
32
|
+
_DOCUMENTCLASS_RE = None # 惰性编译(见 _is_main_candidate)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EprintError(Exception):
|
|
36
|
+
"""e-print 取数/解包失败(消息可直接进用户输出)。"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_main_candidate(name: str, text: str) -> bool:
|
|
40
|
+
global _DOCUMENTCLASS_RE
|
|
41
|
+
if _DOCUMENTCLASS_RE is None:
|
|
42
|
+
import re
|
|
43
|
+
_DOCUMENTCLASS_RE = re.compile(r"\\documentclass")
|
|
44
|
+
return _DOCUMENTCLASS_RE.search(text) is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_url(url: str) -> None:
|
|
48
|
+
"""仅允许白名单域名的 http(s) URL(拒环回/私有/保留地址)。"""
|
|
49
|
+
from urllib.parse import urlparse
|
|
50
|
+
p = urlparse(url)
|
|
51
|
+
if p.scheme not in ("http", "https"):
|
|
52
|
+
raise EprintError(f"仅允许 http(s) URL:{url[:80]}")
|
|
53
|
+
host = (p.hostname or "").lower()
|
|
54
|
+
if host not in _ALLOWED_HOSTS:
|
|
55
|
+
raise EprintError(f"域名不在白名单({'/'.join(sorted(_ALLOWED_HOSTS))}):{host}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _safe_name(name: str) -> bool:
|
|
59
|
+
"""tar 成员名安全判定:拒绝对路径/盘符/穿越(只读内存态,防御性双保险)。"""
|
|
60
|
+
if not name or name.startswith(("/", "\\")):
|
|
61
|
+
return False
|
|
62
|
+
if ":" in name.split("/")[0]: # C:\ 盘符形态
|
|
63
|
+
return False
|
|
64
|
+
parts = name.replace("\\", "/").split("/")
|
|
65
|
+
return ".." not in parts
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _pick_main(texts: dict[str, str]) -> str | None:
|
|
69
|
+
"""主 tex:含 \\documentclass 的最大文件;没有则最大的 .tex。"""
|
|
70
|
+
candidates = [n for n in texts if n.endswith(".tex")]
|
|
71
|
+
if not candidates:
|
|
72
|
+
return None
|
|
73
|
+
doc_class = [n for n in candidates if _is_main_candidate(n, texts[n])]
|
|
74
|
+
pool = doc_class or candidates
|
|
75
|
+
return max(pool, key=lambda n: len(texts[n]))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_eprint(body: bytes, *, max_members: int = _MAX_MEMBERS,
|
|
79
|
+
max_total: int = _MAX_TOTAL_UNCOMPRESSED,
|
|
80
|
+
max_file: int = _MAX_FILE_UNCOMPRESSED) -> dict[str, Any]:
|
|
81
|
+
"""解析 e-print 字节流(tar.gz / 单文件 gzip / 纯 tex),全内存态。
|
|
82
|
+
|
|
83
|
+
返回 {format, files:[{name,size}], texts:{name:content}, rejected:[...]},
|
|
84
|
+
非 LaTeX 源包(如误传 PDF)抛 EprintError。
|
|
85
|
+
"""
|
|
86
|
+
if body[:5] == b"%PDF-":
|
|
87
|
+
raise EprintError("这是 PDF 不是 LaTeX 源包(e-print 端点才返回源码)")
|
|
88
|
+
tf = None
|
|
89
|
+
try:
|
|
90
|
+
try:
|
|
91
|
+
tf = tarfile.open(fileobj=io.BytesIO(body), mode="r:*")
|
|
92
|
+
members = [(m.name, m.size, tf.extractfile(m)) for m in tf.getmembers()]
|
|
93
|
+
fmt = "tar"
|
|
94
|
+
except tarfile.ReadError:
|
|
95
|
+
if body[:2] == b"\x1f\x8b": # 单文件 gzip(老论文常见:单个 .tex.gz)
|
|
96
|
+
text = gzip.decompress(body).decode("utf-8", errors="replace")
|
|
97
|
+
name = "main.tex"
|
|
98
|
+
return {"format": "gz", "main_file": name,
|
|
99
|
+
"files": [{"name": name, "size": len(text)}],
|
|
100
|
+
"texts": {name: text}, "rejected": []}
|
|
101
|
+
text = body.decode("utf-8", errors="replace")
|
|
102
|
+
return {"format": "plain", "main_file": "main.tex",
|
|
103
|
+
"files": [{"name": "main.tex", "size": len(text)}],
|
|
104
|
+
"texts": {"main.tex": text}, "rejected": []}
|
|
105
|
+
except EprintError:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise EprintError(f"源码包解析失败: {str(e)[:120]}")
|
|
109
|
+
|
|
110
|
+
files: list[dict[str, Any]] = []
|
|
111
|
+
texts: dict[str, str] = {}
|
|
112
|
+
rejected: list[str] = []
|
|
113
|
+
total = 0
|
|
114
|
+
if len(members) > max_members:
|
|
115
|
+
raise EprintError(f"成员数 {len(members)} 超上限 {max_members},疑似恶意包")
|
|
116
|
+
for raw_name, size, fh in members:
|
|
117
|
+
if not _safe_name(raw_name):
|
|
118
|
+
rejected.append(raw_name)
|
|
119
|
+
continue
|
|
120
|
+
if size > max_file:
|
|
121
|
+
rejected.append(f"{raw_name}(单文件 {size} 超上限)")
|
|
122
|
+
continue
|
|
123
|
+
total += size
|
|
124
|
+
if total > max_total:
|
|
125
|
+
raise EprintError(f"解压总量超上限 {max_total // (1024 * 1024)}MB")
|
|
126
|
+
if not raw_name.lower().endswith(_TEX_EXTS) or fh is None:
|
|
127
|
+
files.append({"name": raw_name, "size": size})
|
|
128
|
+
continue
|
|
129
|
+
data = fh.read(max_file + 1)
|
|
130
|
+
if len(data) > max_file:
|
|
131
|
+
rejected.append(f"{raw_name}(解压超限)")
|
|
132
|
+
continue
|
|
133
|
+
text = data.decode("utf-8", errors="replace")
|
|
134
|
+
files.append({"name": raw_name, "size": size})
|
|
135
|
+
texts[raw_name] = text
|
|
136
|
+
main = _pick_main(texts)
|
|
137
|
+
return {"format": fmt, "files": files, "texts": texts,
|
|
138
|
+
"main_file": main, "rejected": rejected}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def fetch_eprint(arxiv_id: str, use_cache: bool = True) -> dict[str, Any]:
|
|
142
|
+
"""拉取并解析 arXiv 源码包;带 fetch 缓存(1h 内二读零成本)。
|
|
143
|
+
|
|
144
|
+
返回 parse_eprint 结果 + {"content_warning": ...};texts 总量超缓存上限时
|
|
145
|
+
只缓存清单并置 texts_omitted(本次调用仍返回全文)。
|
|
146
|
+
"""
|
|
147
|
+
aid = arxiv_id.strip()
|
|
148
|
+
if not aid or any(c.isspace() for c in aid):
|
|
149
|
+
raise EprintError(f"arXiv id 形态不对:{aid[:40]}")
|
|
150
|
+
url = f"https://arxiv.org/e-print/{aid}"
|
|
151
|
+
_validate_url(url)
|
|
152
|
+
|
|
153
|
+
cache = None
|
|
154
|
+
cache_key = f"arxiv-eprint:{aid}"
|
|
155
|
+
if use_cache:
|
|
156
|
+
try:
|
|
157
|
+
from cache import SearchCache
|
|
158
|
+
cache = SearchCache()
|
|
159
|
+
hit = cache.get_fetch(cache_key)
|
|
160
|
+
if isinstance(hit, dict) and hit.get("files"):
|
|
161
|
+
hit["_cached"] = True
|
|
162
|
+
return hit
|
|
163
|
+
except Exception:
|
|
164
|
+
cache = None
|
|
165
|
+
|
|
166
|
+
with open_url(url, timeout=60) as resp:
|
|
167
|
+
body = resp.read(_MAX_DOWNLOAD + 1)
|
|
168
|
+
if len(body) > _MAX_DOWNLOAD:
|
|
169
|
+
raise EprintError(f"源码包超过下载上限 {_MAX_DOWNLOAD // (1024 * 1024)}MB")
|
|
170
|
+
|
|
171
|
+
result = parse_eprint(body)
|
|
172
|
+
result["arxiv_id"] = aid
|
|
173
|
+
result["content_warning"] = CONTENT_WARNING
|
|
174
|
+
if cache is not None:
|
|
175
|
+
try:
|
|
176
|
+
payload = {k: v for k, v in result.items() if k != "_cached"}
|
|
177
|
+
if sum(len(t) for t in payload.get("texts", {}).values()) > _MAX_CACHE_TEXTS:
|
|
178
|
+
payload["texts"] = {}
|
|
179
|
+
payload["texts_omitted"] = True
|
|
180
|
+
cache.set_fetch(cache_key, payload)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass # 缓存写失败不影响本次结果
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def latex_read(source: dict[str, Any], file: str | None = None,
|
|
187
|
+
start: int = 0, max_chars: int | None = None) -> dict[str, Any]:
|
|
188
|
+
"""从已解析的源码包按文件分页读取(默认主 tex);chunk 契约与 pdf 相同。"""
|
|
189
|
+
texts = source.get("texts") or {}
|
|
190
|
+
name = file or source.get("main_file")
|
|
191
|
+
if not name:
|
|
192
|
+
raise EprintError("源码包内没有可读的 .tex 文件")
|
|
193
|
+
if name not in texts:
|
|
194
|
+
if source.get("texts_omitted"):
|
|
195
|
+
raise EprintError("该源码包文本未入缓存(体积超限),请重新拉取")
|
|
196
|
+
available = ", ".join(sorted(texts))[:200] or "(无 .tex 成员)"
|
|
197
|
+
raise EprintError(f"文件 {name} 不在源码包内。可用:{available}")
|
|
198
|
+
seg = chunk_text(texts[name], start, max_chars or 12000)
|
|
199
|
+
return {"content_warning": CONTENT_WARNING, "file": name,
|
|
200
|
+
"main_file": source.get("main_file"), "source_format": source.get("format"),
|
|
201
|
+
**seg}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""badcase_ledger — 坏例账本:把分散三处的失败统一入账、可查、可做发布门禁。
|
|
3
|
+
|
|
4
|
+
定位(2026-10-11 方案丙):argo 的失败证据分散在三处、形态各异——
|
|
5
|
+
1. usage_log/query.jsonl:errors[] 非空或 count=0 的真实查询(含 serp_guard
|
|
6
|
+
整页误杀外露,见 search.py drain 处);
|
|
7
|
+
2. usage_log/recovery.jsonl:触发过恢复链的查询(triggered=true);
|
|
8
|
+
3. circuit_breaker.json:state != closed 的引擎(点状快照,引擎级坏例)。
|
|
9
|
+
本工具把它们归一进 SQLite 账本(<state>/badcase.db),支持 ingest(幂等去重)、
|
|
10
|
+
list、stats 与 check(发布前门禁:近 N 天新增坏例超阈值即非零退出)。
|
|
11
|
+
|
|
12
|
+
与 replay_eval.py 的分工:replay_eval 用录制重放整条管线评打分质量(离线
|
|
13
|
+
仲裁);本账本只收纳「真实发生的失败」并让它们可归因、可回归——对应评测
|
|
14
|
+
体系的 badcase 管理环节:发现 → 入账 → 归因 → 修复后 check 不复发。
|
|
15
|
+
|
|
16
|
+
数据卫生:engines_used 含表外引擎名(夹具/拼写错)的记录不入账——账本只收
|
|
17
|
+
真实坏例,与 usage_log_clean 的隔离口径同源(engines.available_engines)。
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
python3 scripts/badcase_ledger.py ingest # 三源归一入账(幂等)
|
|
21
|
+
python3 scripts/badcase_ledger.py list --kind query_error --limit 10
|
|
22
|
+
python3 scripts/badcase_ledger.py stats # 按 kind/引擎聚合
|
|
23
|
+
python3 scripts/badcase_ledger.py check --since 7d --max 50 # 发布门禁
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import sqlite3
|
|
31
|
+
import sys
|
|
32
|
+
from collections import Counter
|
|
33
|
+
from datetime import datetime, timedelta
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
38
|
+
|
|
39
|
+
import argo_paths # noqa: E402
|
|
40
|
+
from engines import available_engines # noqa: E402
|
|
41
|
+
from usage_log import usage_log_dir # noqa: E402
|
|
42
|
+
|
|
43
|
+
_CANON = frozenset(available_engines())
|
|
44
|
+
|
|
45
|
+
_SCHEMA = (
|
|
46
|
+
"CREATE TABLE IF NOT EXISTS badcases ("
|
|
47
|
+
" id INTEGER PRIMARY KEY,"
|
|
48
|
+
" kind TEXT NOT NULL," # query_error | empty_result | recovery | breaker
|
|
49
|
+
" ts TEXT NOT NULL," # 原始 ISO 时间串;breaker 快照用入账时刻
|
|
50
|
+
" query TEXT NOT NULL DEFAULT '',"
|
|
51
|
+
" engine TEXT NOT NULL DEFAULT '',"
|
|
52
|
+
" detail TEXT NOT NULL DEFAULT '',"
|
|
53
|
+
" fingerprint TEXT NOT NULL UNIQUE)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def db_path() -> Path:
|
|
58
|
+
return argo_paths.state_path("badcase.db")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _connect() -> sqlite3.Connection:
|
|
62
|
+
con = sqlite3.connect(str(db_path()), timeout=8)
|
|
63
|
+
con.execute(_SCHEMA)
|
|
64
|
+
return con
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _rows_from_query_log(path: Path) -> list[dict[str, Any]]:
|
|
68
|
+
if not path.exists():
|
|
69
|
+
return []
|
|
70
|
+
out = []
|
|
71
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
72
|
+
if not line.strip():
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
r = json.loads(line)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
continue
|
|
78
|
+
raw_used = r.get("engines_used") or []
|
|
79
|
+
used = [e for e in raw_used if e in _CANON]
|
|
80
|
+
if not used and raw_used:
|
|
81
|
+
continue # 表外名占满 → 夹具行,不入账(口径同 usage_log_clean)
|
|
82
|
+
raw_errors = r.get("errors")
|
|
83
|
+
# 生产日志里 errors 有两种形态:字符串列表(主流)与 int 计数(部分
|
|
84
|
+
# 旧条目)——统一成字符串列表再取前几条
|
|
85
|
+
errors = (raw_errors if isinstance(raw_errors, list)
|
|
86
|
+
else ([str(raw_errors)] if raw_errors else []))
|
|
87
|
+
if not errors and r.get("count"):
|
|
88
|
+
continue # 正常条目不是坏例
|
|
89
|
+
if errors:
|
|
90
|
+
out.append({"kind": "query_error", "ts": r.get("ts", ""),
|
|
91
|
+
"query": r.get("query", ""),
|
|
92
|
+
"engine": ",".join(used[:3]),
|
|
93
|
+
"detail": " | ".join(str(e)[:160] for e in errors[:3])})
|
|
94
|
+
else:
|
|
95
|
+
out.append({"kind": "empty_result", "ts": r.get("ts", ""),
|
|
96
|
+
"query": r.get("query", ""),
|
|
97
|
+
"engine": ",".join(used[:3]),
|
|
98
|
+
"detail": f"count=0, mode={r.get('mode')}"})
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _rows_from_recovery_log(path: Path) -> list[dict[str, Any]]:
|
|
103
|
+
if not path.exists():
|
|
104
|
+
return []
|
|
105
|
+
out = []
|
|
106
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
107
|
+
if not line.strip():
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
r = json.loads(line)
|
|
111
|
+
except json.JSONDecodeError:
|
|
112
|
+
continue
|
|
113
|
+
if not r.get("triggered"):
|
|
114
|
+
continue
|
|
115
|
+
out.append({"kind": "recovery", "ts": r.get("ts", ""),
|
|
116
|
+
"query": r.get("query", ""),
|
|
117
|
+
"engine": "",
|
|
118
|
+
"detail": (f"level={r.get('level_used')} "
|
|
119
|
+
f"strategy={r.get('strategy_used')} "
|
|
120
|
+
f"recovered={r.get('recovered')}")})
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _rows_from_breaker(path: Path) -> list[dict[str, Any]]:
|
|
125
|
+
if not path.exists():
|
|
126
|
+
return []
|
|
127
|
+
try:
|
|
128
|
+
engines = json.loads(path.read_text(encoding="utf-8")).get("engines", {})
|
|
129
|
+
except (json.JSONDecodeError, OSError):
|
|
130
|
+
return []
|
|
131
|
+
out = []
|
|
132
|
+
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
133
|
+
for name, st in sorted(engines.items()):
|
|
134
|
+
if st.get("state") == "closed":
|
|
135
|
+
continue
|
|
136
|
+
out.append({"kind": "breaker", "ts": now, "query": "", "engine": name,
|
|
137
|
+
"detail": (f"state={st.get('state')} "
|
|
138
|
+
f"failures={st.get('failures')} "
|
|
139
|
+
f"opens={st.get('opens')}")})
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def ingest() -> dict:
|
|
144
|
+
log_dir = usage_log_dir()
|
|
145
|
+
rows = (_rows_from_query_log(log_dir / "query.jsonl")
|
|
146
|
+
+ _rows_from_recovery_log(log_dir / "recovery.jsonl")
|
|
147
|
+
+ _rows_from_breaker(argo_paths.state_path("circuit_breaker.json")))
|
|
148
|
+
con = _connect()
|
|
149
|
+
added = 0
|
|
150
|
+
try:
|
|
151
|
+
for r in rows:
|
|
152
|
+
fp = hashlib.sha1("|".join(
|
|
153
|
+
[str(r["kind"]), str(r["ts"]), str(r["query"]),
|
|
154
|
+
str(r["engine"]), str(r["detail"])[:120]])
|
|
155
|
+
.encode("utf-8")).hexdigest()
|
|
156
|
+
cur = con.execute(
|
|
157
|
+
"INSERT OR IGNORE INTO badcases(ts, query, engine, detail,"
|
|
158
|
+
" kind, fingerprint) VALUES (?,?,?,?,?,?)",
|
|
159
|
+
(r["ts"], r["query"], r["engine"], r["detail"],
|
|
160
|
+
r["kind"], fp))
|
|
161
|
+
added += cur.rowcount
|
|
162
|
+
total = con.execute("SELECT COUNT(*) FROM badcases").fetchone()[0]
|
|
163
|
+
con.commit()
|
|
164
|
+
finally:
|
|
165
|
+
con.close()
|
|
166
|
+
return {"sources": len(rows), "added": added, "total": total,
|
|
167
|
+
"db": str(db_path())}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_since(since: str) -> datetime:
|
|
171
|
+
"""7d / 24h / 2026-10-01 → datetime(本地时区)。"""
|
|
172
|
+
since = since.strip().lower()
|
|
173
|
+
now = datetime.now().astimezone()
|
|
174
|
+
if since.endswith("d") and since[:-1].isdigit():
|
|
175
|
+
return now - timedelta(days=int(since[:-1]))
|
|
176
|
+
if since.endswith("h") and since[:-1].isdigit():
|
|
177
|
+
return now - timedelta(hours=int(since[:-1]))
|
|
178
|
+
return datetime.fromisoformat(since).astimezone()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _fetch(kind: str | None, since: datetime | None,
|
|
182
|
+
limit: int) -> list[dict[str, Any]]:
|
|
183
|
+
con = _connect()
|
|
184
|
+
try:
|
|
185
|
+
sql = ("SELECT ts, kind, query, engine, detail FROM badcases"
|
|
186
|
+
" WHERE 1=1")
|
|
187
|
+
params: list = []
|
|
188
|
+
if kind:
|
|
189
|
+
sql += " AND kind = ?"
|
|
190
|
+
params.append(kind)
|
|
191
|
+
if since is not None:
|
|
192
|
+
sql += " AND ts >= ?"
|
|
193
|
+
params.append(since.isoformat(timespec="seconds"))
|
|
194
|
+
sql += " ORDER BY ts DESC LIMIT ?"
|
|
195
|
+
params.append(limit)
|
|
196
|
+
keys = ["ts", "kind", "query", "engine", "detail"]
|
|
197
|
+
return [dict(zip(keys, row)) for row in con.execute(sql, params)]
|
|
198
|
+
finally:
|
|
199
|
+
con.close()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def check(since: str, max_new: int) -> dict:
|
|
203
|
+
rows = _fetch(None, _parse_since(since), limit=10_000)
|
|
204
|
+
ok = len(rows) <= max_new
|
|
205
|
+
return {"since": since, "new_badcases": len(rows), "max": max_new,
|
|
206
|
+
"ok": ok}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def stats() -> dict:
|
|
210
|
+
con = _connect()
|
|
211
|
+
try:
|
|
212
|
+
by_kind = dict(con.execute(
|
|
213
|
+
"SELECT kind, COUNT(*) FROM badcases GROUP BY kind").fetchall())
|
|
214
|
+
by_engine = dict(con.execute(
|
|
215
|
+
"SELECT engine, COUNT(*) FROM badcases WHERE engine != ''"
|
|
216
|
+
" GROUP BY engine ORDER BY COUNT(*) DESC LIMIT 8").fetchall())
|
|
217
|
+
total = con.execute("SELECT COUNT(*) FROM badcases").fetchone()[0]
|
|
218
|
+
finally:
|
|
219
|
+
con.close()
|
|
220
|
+
return {"total": total, "by_kind": by_kind, "top_engines": by_engine}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main(argv: list[str] | None = None) -> int:
|
|
224
|
+
ap = argparse.ArgumentParser(description="坏例账本(badcase 管理)")
|
|
225
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
226
|
+
sub.add_parser("ingest")
|
|
227
|
+
p_list = sub.add_parser("list")
|
|
228
|
+
p_list.add_argument("--kind", default=None,
|
|
229
|
+
choices=["query_error", "empty_result",
|
|
230
|
+
"recovery", "breaker"])
|
|
231
|
+
p_list.add_argument("--limit", type=int, default=10)
|
|
232
|
+
sub.add_parser("stats")
|
|
233
|
+
p_check = sub.add_parser("check")
|
|
234
|
+
p_check.add_argument("--since", default="7d")
|
|
235
|
+
p_check.add_argument("--max", type=int, default=50)
|
|
236
|
+
args = ap.parse_args(argv)
|
|
237
|
+
if args.cmd == "ingest":
|
|
238
|
+
print(json.dumps(ingest(), ensure_ascii=False))
|
|
239
|
+
elif args.cmd == "list":
|
|
240
|
+
for row in _fetch(args.kind, None, args.limit):
|
|
241
|
+
print(json.dumps(row, ensure_ascii=False))
|
|
242
|
+
elif args.cmd == "stats":
|
|
243
|
+
print(json.dumps(stats(), ensure_ascii=False))
|
|
244
|
+
elif args.cmd == "check":
|
|
245
|
+
report = check(args.since, args.max)
|
|
246
|
+
print(json.dumps(report, ensure_ascii=False))
|
|
247
|
+
return 0 if report["ok"] else 1
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
sys.exit(main())
|