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,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""fact_crosscheck.py — 数字交叉验证(方案 A)。
|
|
3
|
+
|
|
4
|
+
问题:多源搜索结果中,同一数字可能不一致(如「营收 100 亿」vs「营收 120 亿」),
|
|
5
|
+
Agent 无法判断哪个可信。本模块用纯 stdlib 做本地对比,零外部服务。
|
|
6
|
+
|
|
7
|
+
设计约束:
|
|
8
|
+
- 零外部依赖,只用 stdlib(re)
|
|
9
|
+
- 纯函数,无全局状态
|
|
10
|
+
- 输出:{has_conflict, conflict(别名), indicator, sources, values, suggestion}
|
|
11
|
+
——sources[i] 与 values[i] 同序配对(第 i 个值来自第 i 个源)
|
|
12
|
+
|
|
13
|
+
防复发说明(2026-10-16 重建):
|
|
14
|
+
旧文件因多次 Edit 追加 _CORE_INDICATORS 导致 2420 行、集合未闭合、
|
|
15
|
+
尾部函数体丢失。本文件把指标词表收敛到最小核心(~30 词,去重),
|
|
16
|
+
全文件 <200 行,受 test_module_size_gate.py(1000 行上限)约束。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
# 数字提取正则:匹配「数字 + 单位」模式
|
|
24
|
+
_NUMBER_RE = re.compile(
|
|
25
|
+
r"(\d+(?:\.\d+)?)\s*"
|
|
26
|
+
r"(亿|万|千|百|元|美元|股|%|%|倍|年|月|日|个|家|人|吨|克|千克|米|千米|平方|立方)?"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# 核心指标词表:唯一来源 backends/text_lexicon.json(经 text_lexicon 延迟加载)。
|
|
30
|
+
# 原则:宁缺勿滥——未知词走 ""(无指标),不误报;新增词改 JSON + 补测试用例,不改代码。
|
|
31
|
+
def _core_indicators() -> frozenset[str]:
|
|
32
|
+
from text_lexicon import core_indicators
|
|
33
|
+
return core_indicators()
|
|
34
|
+
|
|
35
|
+
# 时间/口径范围词:不同口径的数字不得直接对比(防「生涯 350 vs 单季 15」误报)。
|
|
36
|
+
_CAREER_MARKERS = ("职业生涯", "生涯", "历史总", "累计", "总数")
|
|
37
|
+
_YEAR_RE = re.compile(r"(19|20)\d{2}")
|
|
38
|
+
_SEASON_RE = re.compile(r"\d{4}\s*赛季|赛季|单季|本季")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _normalize_value(value: float, unit: str) -> float:
|
|
42
|
+
"""归一化到同一量级。未知单位原样返回。"""
|
|
43
|
+
if unit == "亿":
|
|
44
|
+
return value * 1e8
|
|
45
|
+
if unit == "万":
|
|
46
|
+
return value * 1e4
|
|
47
|
+
if unit == "千":
|
|
48
|
+
return value * 1e3
|
|
49
|
+
if unit == "百":
|
|
50
|
+
return value * 1e2
|
|
51
|
+
return float(value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_indicator(text: str) -> str:
|
|
55
|
+
"""从文本中提取第一个命中的核心指标,无命中返回 ""。长词优先。"""
|
|
56
|
+
if not text:
|
|
57
|
+
return ""
|
|
58
|
+
for ind in sorted(_core_indicators(), key=len, reverse=True):
|
|
59
|
+
if ind and ind in text:
|
|
60
|
+
return ind
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _extract_scope(text: str) -> str:
|
|
65
|
+
"""提取口径范围,用于判断两个数字是否可比。
|
|
66
|
+
|
|
67
|
+
返回:career / 年份如 "2024" / season / ""(无口径信息)。
|
|
68
|
+
"""
|
|
69
|
+
if not text:
|
|
70
|
+
return ""
|
|
71
|
+
if any(m in text for m in _CAREER_MARKERS):
|
|
72
|
+
return "career"
|
|
73
|
+
m = _YEAR_RE.search(text)
|
|
74
|
+
if m:
|
|
75
|
+
return m.group(0)
|
|
76
|
+
if _SEASON_RE.search(text):
|
|
77
|
+
return "season"
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _indicator_near(text: str, start: int, end: int, window: int = 12) -> str:
|
|
82
|
+
"""取数字邻近窗口内的指标(防标题词污染片段词)。
|
|
83
|
+
|
|
84
|
+
例:标题「进球总数」+ 片段「助攻 350 次」→ 数字 350 邻近的是「助攻」,
|
|
85
|
+
不得取标题里的「进球」。无命中返回 ""(宁缺勿滥,不误报)。
|
|
86
|
+
"""
|
|
87
|
+
lo = max(0, start - window)
|
|
88
|
+
hi = min(len(text), end + window // 2)
|
|
89
|
+
win = text[lo:hi]
|
|
90
|
+
num_pos = start - lo # 数字在窗口内的偏移
|
|
91
|
+
best = ""
|
|
92
|
+
best_key = None # (距离, -长度)
|
|
93
|
+
for ind in _core_indicators():
|
|
94
|
+
if not ind:
|
|
95
|
+
continue
|
|
96
|
+
pos = win.find(ind)
|
|
97
|
+
while pos != -1:
|
|
98
|
+
dist = abs(pos - num_pos)
|
|
99
|
+
key = (dist, -len(ind))
|
|
100
|
+
if best_key is None or key < best_key:
|
|
101
|
+
best_key = key
|
|
102
|
+
best = ind
|
|
103
|
+
pos = win.find(ind, pos + 1)
|
|
104
|
+
return best
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _extract_numbers(text: str) -> list[dict[str, Any]]:
|
|
108
|
+
"""从文本提取数字列表,每项 {value, unit, raw, indicator, scope}。"""
|
|
109
|
+
if not text:
|
|
110
|
+
return []
|
|
111
|
+
scope = _extract_scope(text)
|
|
112
|
+
out: list[dict[str, Any]] = []
|
|
113
|
+
for m in _NUMBER_RE.finditer(text):
|
|
114
|
+
try:
|
|
115
|
+
value = float(m.group(1))
|
|
116
|
+
except ValueError:
|
|
117
|
+
continue
|
|
118
|
+
unit = m.group(2) or ""
|
|
119
|
+
# 跳过年份/日期数字(防「2024 营收」里的 2024 参与数值对比)
|
|
120
|
+
if unit in ("年", "月", "日") and 1900 <= value <= 2100:
|
|
121
|
+
continue
|
|
122
|
+
if not unit and 1900 <= value <= 2100 and len(m.group(1)) == 4:
|
|
123
|
+
continue
|
|
124
|
+
indicator = _indicator_near(text, m.start(), m.end())
|
|
125
|
+
out.append({
|
|
126
|
+
"value": value,
|
|
127
|
+
"unit": unit,
|
|
128
|
+
"raw": m.group(0),
|
|
129
|
+
"indicator": indicator,
|
|
130
|
+
"scope": scope,
|
|
131
|
+
})
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _values_conflict(vals: list[float], rel_tol: float = 0.05) -> bool:
|
|
136
|
+
"""归一化后最大值与最小值相对差异超过 rel_tol 即判冲突。"""
|
|
137
|
+
if len(vals) < 2:
|
|
138
|
+
return False
|
|
139
|
+
lo, hi = min(vals), max(vals)
|
|
140
|
+
base = max(abs(hi), 1e-9)
|
|
141
|
+
return (hi - lo) / base > rel_tol
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _comparable_buckets(
|
|
145
|
+
items: list[tuple[float, str, str]],
|
|
146
|
+
) -> list[list[tuple[float, str]]]:
|
|
147
|
+
"""把 (值, scope, 源) 列表切成可比桶。
|
|
148
|
+
|
|
149
|
+
口径兼容规则(docstring 口径,唯一实现):
|
|
150
|
+
- 有 scope 的只与同 scope 比;
|
|
151
|
+
- scope 为空(无口径信息)的与每个有口径组分别比;
|
|
152
|
+
- 全无 scope 时合成一桶互相比。
|
|
153
|
+
|
|
154
|
+
返回的每桶是 [(值, 源)],桶内同序配对。
|
|
155
|
+
"""
|
|
156
|
+
scoped: dict[str, list[tuple[float, str]]] = {}
|
|
157
|
+
for norm, scope, src in items:
|
|
158
|
+
scoped.setdefault(scope or "__none__", []).append((norm, src))
|
|
159
|
+
nones = scoped.get("__none__", [])
|
|
160
|
+
buckets: list[list[tuple[float, str]]] = []
|
|
161
|
+
for scope, lst in scoped.items():
|
|
162
|
+
if scope != "__none__":
|
|
163
|
+
buckets.append(lst + nones)
|
|
164
|
+
if nones and not buckets:
|
|
165
|
+
buckets.append(nones)
|
|
166
|
+
return buckets
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def crosscheck_numbers(
|
|
170
|
+
query: str, candidates: list[dict[str, Any]]
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""交叉验证候选中的数字是否矛盾。
|
|
173
|
+
|
|
174
|
+
可比条件(同时满足才比较):
|
|
175
|
+
1. 候选数字的 indicator 相同且非空;
|
|
176
|
+
2. 若 query 能提取到指标,只比较与 query 同指标的数字
|
|
177
|
+
(防「查进球、片段里助攻 350 vs 15」误报);
|
|
178
|
+
3. scope 兼容:至少一方无口径信息,或双方口径相同。
|
|
179
|
+
career 与具体年份/赛季视为不兼容。
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
{has_conflict, conflict(别名), sources, values, suggestion, indicator}
|
|
183
|
+
"""
|
|
184
|
+
empty: dict[str, Any] = {
|
|
185
|
+
"has_conflict": False,
|
|
186
|
+
"conflict": False,
|
|
187
|
+
"sources": [],
|
|
188
|
+
"values": [],
|
|
189
|
+
"suggestion": "",
|
|
190
|
+
"indicator": "",
|
|
191
|
+
}
|
|
192
|
+
if not candidates:
|
|
193
|
+
return empty
|
|
194
|
+
|
|
195
|
+
query_ind = _extract_indicator(query or "")
|
|
196
|
+
|
|
197
|
+
# 按 (indicator) 分组收集
|
|
198
|
+
groups: dict[str, list[tuple[float, str, str]]] = {}
|
|
199
|
+
for c in candidates:
|
|
200
|
+
text = f"{c.get('title', '')} {c.get('snippet', '')}"
|
|
201
|
+
for n in _extract_numbers(text):
|
|
202
|
+
ind = n["indicator"]
|
|
203
|
+
if not ind:
|
|
204
|
+
continue
|
|
205
|
+
if query_ind and ind != query_ind:
|
|
206
|
+
continue
|
|
207
|
+
norm = _normalize_value(n["value"], n["unit"])
|
|
208
|
+
groups.setdefault(ind, []).append(
|
|
209
|
+
(norm, n["scope"], str(c.get("source", "")))
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for ind, items in groups.items():
|
|
213
|
+
for bucket in _comparable_buckets(items):
|
|
214
|
+
vals = [v for v, _ in bucket]
|
|
215
|
+
if _values_conflict(vals):
|
|
216
|
+
srcs = [s for _, s in bucket]
|
|
217
|
+
sug = (
|
|
218
|
+
f"同一指标「{ind}」下多源数值分歧 "
|
|
219
|
+
f"({min(vals):g} ~ {max(vals):g}),建议 fetch 原文核对口径后再引用。"
|
|
220
|
+
)
|
|
221
|
+
return {
|
|
222
|
+
"has_conflict": True,
|
|
223
|
+
"conflict": True,
|
|
224
|
+
"sources": srcs,
|
|
225
|
+
"values": vals,
|
|
226
|
+
"suggestion": sug,
|
|
227
|
+
"indicator": ind,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return empty
|
package/scripts/fetch.py
CHANGED
|
@@ -7,11 +7,10 @@ fetch.py — 轻量页面抓取(纯标准库)
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import re
|
|
11
10
|
import urllib.request
|
|
12
11
|
from html.parser import HTMLParser
|
|
13
12
|
from typing import Any
|
|
14
|
-
from
|
|
13
|
+
from net_proxy import open_url # 出口调度唯一入口(issue #13 同类修复)
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class ContentExtractor(HTMLParser):
|
|
@@ -55,12 +54,20 @@ def fetch_page(url: str, max_chars: int = 3000, timeout: int = 8,
|
|
|
55
54
|
timeout: 超时秒数
|
|
56
55
|
raw: True 时返回原始 HTML(用于 extract/crawl_sitemap 等需要 HTML 的场景)
|
|
57
56
|
"""
|
|
57
|
+
try:
|
|
58
|
+
from url_safety import check_url
|
|
59
|
+
ok, reason = check_url(url)
|
|
60
|
+
if not ok:
|
|
61
|
+
return {"url": url, "content": "", "html": "", "length": 0,
|
|
62
|
+
"success": False, "error": f"URL 被 SSRF 防护拦截: {reason}"}
|
|
63
|
+
except ImportError:
|
|
64
|
+
pass
|
|
58
65
|
try:
|
|
59
66
|
req = urllib.request.Request(url, headers={
|
|
60
67
|
"User-Agent": "unified-search/2.5 (+local-research)",
|
|
61
68
|
"Accept": "text/html,application/xhtml+xml,application/xml",
|
|
62
69
|
})
|
|
63
|
-
with
|
|
70
|
+
with open_url(req, timeout=timeout) as resp:
|
|
64
71
|
content_type = resp.headers.get("Content-Type", "")
|
|
65
72
|
if "pdf" in content_type:
|
|
66
73
|
return {"url": url, "content": "", "html": "", "length": 0, "success": False, "error": "PDF not supported"}
|
|
@@ -92,17 +99,23 @@ def fetch_page(url: str, max_chars: int = 3000, timeout: int = 8,
|
|
|
92
99
|
|
|
93
100
|
def fetch_pages_parallel(urls: list[str], max_chars: int = 3000,
|
|
94
101
|
timeout: int = 8, max_workers: int = 3) -> list[dict[str, Any]]:
|
|
95
|
-
"""
|
|
96
|
-
from
|
|
102
|
+
"""并行抓取多个页面。总时限 timeout+3 秒,到点未完成的页面以失败结果占位返回。"""
|
|
103
|
+
from bounded_run import run_bounded, TaskError
|
|
104
|
+
|
|
105
|
+
if not urls:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
def _fail(url: str) -> dict:
|
|
109
|
+
return {"url": url, "content": "", "length": 0, "success": False}
|
|
97
110
|
|
|
111
|
+
finished, unfinished = run_bounded(
|
|
112
|
+
urls, lambda u: fetch_page(u, max_chars, timeout),
|
|
113
|
+
timeout + 3, max_workers=min(len(urls), max_workers))
|
|
98
114
|
results = []
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
results.append(fut.result())
|
|
104
|
-
except Exception:
|
|
105
|
-
results.append({"url": futures[fut], "content": "", "length": 0, "success": False})
|
|
115
|
+
for url, page in finished:
|
|
116
|
+
results.append(_fail(url) if isinstance(page, TaskError) else page)
|
|
117
|
+
for url in unfinished:
|
|
118
|
+
results.append(_fail(url))
|
|
106
119
|
return results
|
|
107
120
|
|
|
108
121
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""fetch_browser.py — fetch 降档链第二级B:Chrome CDP 浏览器档
|
|
3
|
+
|
|
4
|
+
从 fetch_v3 拆出(fetch_v3 在模块体量门禁的祖父清单里,只能减不能增;
|
|
5
|
+
浏览器档是自洽的一级,与 chrome_cdp / browser_auth 同域)。
|
|
6
|
+
|
|
7
|
+
职责:
|
|
8
|
+
- 系统 Chrome(headless)渲染取页 + Hound 风格 actions 交互
|
|
9
|
+
- 登录态车道(A2):auth_profile 传入 browser_auth 持久 profile 时带会话
|
|
10
|
+
抓取,结果标 login_state_used / cache_eligible=False——cache.assert_cacheable
|
|
11
|
+
据此拒绝该载荷进公共缓存;cookie 由 Chrome 自持加密,argo 不读取
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _fail(url: str, error: str) -> dict:
|
|
18
|
+
"""失败结果(与 fetch_v3._make_result 的空内容形状逐字段一致)。"""
|
|
19
|
+
return {
|
|
20
|
+
"url": url, "content": "", "html": "", "title": "",
|
|
21
|
+
"length": 0, "success": False, "error": error,
|
|
22
|
+
"fetch_method": "browser",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def browser_fetch(url: str, max_chars: int = 8000, timeout: float = 15.0,
|
|
27
|
+
actions: list[dict] | None = None,
|
|
28
|
+
auth_profile: str | None = None) -> dict:
|
|
29
|
+
"""使用 Chrome CDP 驱动抓取(支持页面交互)。
|
|
30
|
+
|
|
31
|
+
auth_profile:browser_auth 的持久 profile 路径。传入时 Chrome 带登录态
|
|
32
|
+
启动(user_data_dir 指向持久目录;chrome_cdp 对持久 profile 不做清理)。
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
from chrome_cdp import ChromeCDP
|
|
36
|
+
except ImportError:
|
|
37
|
+
return _fail(url, "chrome_cdp not available")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
cdp = ChromeCDP(auto_start=True, user_data_dir=auth_profile)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
err = f"Chrome failed to start: {str(e)[:100]}"
|
|
43
|
+
if auth_profile:
|
|
44
|
+
err += "(登录态 profile 启动失败:关掉占用它的 Chrome,或 argo auth logout 后重登)"
|
|
45
|
+
return _fail(url, err)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# 导航(真 networkidle:CDP Network 事件计数;SPA 永不 idle 时超时放行)
|
|
49
|
+
cdp.navigate(url, wait_until="networkidle")
|
|
50
|
+
|
|
51
|
+
# 执行页面交互序列(Hound actions 等价能力)
|
|
52
|
+
if actions:
|
|
53
|
+
cdp.execute_actions(actions)
|
|
54
|
+
|
|
55
|
+
# 提取内容
|
|
56
|
+
html = cdp.get_html()
|
|
57
|
+
text = cdp.get_text()
|
|
58
|
+
title = cdp.get_title()
|
|
59
|
+
|
|
60
|
+
result = {
|
|
61
|
+
"url": url,
|
|
62
|
+
"content": text[:max_chars] if text else "",
|
|
63
|
+
"html": html[:max_chars * 2] if html else "",
|
|
64
|
+
"title": title or "",
|
|
65
|
+
"length": len(text) if text else 0,
|
|
66
|
+
"success": bool(text),
|
|
67
|
+
"error": None if text else "empty content",
|
|
68
|
+
"fetch_method": "chrome_cdp",
|
|
69
|
+
}
|
|
70
|
+
if auth_profile:
|
|
71
|
+
# 登录态 provenance:与 ego-browser 通道的 envelope 语义同源
|
|
72
|
+
# (candidate_envelope:login_state_used → visibility=authenticated)
|
|
73
|
+
result["login_state_used"] = True
|
|
74
|
+
result["cache_eligible"] = False
|
|
75
|
+
try:
|
|
76
|
+
from browser_auth import touch
|
|
77
|
+
touch(url)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
return result
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return _fail(url, f"CDP error: {str(e)[:100]}")
|
|
83
|
+
finally:
|
|
84
|
+
try:
|
|
85
|
+
cdp.stop()
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""fetch_jsrun.py — fetch 降级链第一级E:V8 JS 执行(js-run 轻量车道)。
|
|
3
|
+
|
|
4
|
+
从 fetch_v3.py 拆出(2026-10-05):jsrun 段独立成模块,fetch_v3.py
|
|
5
|
+
行数回到祖父文件上限内。职责:用 V8 沙箱执行「环境探测 + 纯计算」型
|
|
6
|
+
挑战页 JS,尝试获取 clearance cookie 后重试请求。失败时返回空结果,
|
|
7
|
+
由调用方降级到 tinyfish/CDP。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from engine_env import env_flag
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _jsrun_enabled() -> bool:
|
|
19
|
+
"""js-run 车道开关:ARGO_FETCH_JSRUN=0 关闭,默认开启。"""
|
|
20
|
+
return env_flag("ARGO_FETCH_JSRUN")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _jsrun_challenge_fetch(url: str, html: str, max_chars: int = 8000,
|
|
24
|
+
timeout: float = 8.0,
|
|
25
|
+
backoff_budget_s: float | None = None) -> dict:
|
|
26
|
+
"""用 js-run 执行挑战页 JS,尝试获取 clearance cookie 后重试请求。
|
|
27
|
+
|
|
28
|
+
只处理「环境探测 + 纯计算」型挑战脚本(v0 面)。失败时返回空结果,
|
|
29
|
+
由调用方降级到 tinyfish/CDP。
|
|
30
|
+
|
|
31
|
+
重试请求用 curl_cffi Chrome 指纹 impersonate(站点按 TLS 指纹风控,
|
|
32
|
+
裸 Python 指纹被静默拦截)。退避重试 3 次(间隔 5s/10s)。
|
|
33
|
+
`backoff_budget_s`(秒)给整段重试设墙钟上界:传入时退避睡眠与后续
|
|
34
|
+
尝试都不得越过它——此前 sleep(5)+sleep(10) 不受链级预算约束,cookie
|
|
35
|
+
拿到而三连败时可在 fetch 链内部多耗 ~30s(2026-10-06 审查定位)。
|
|
36
|
+
"""
|
|
37
|
+
import re as _re
|
|
38
|
+
import time as _time
|
|
39
|
+
|
|
40
|
+
# 提取 <script> 内容(挑战页的通行证计算脚本)
|
|
41
|
+
scripts = _re.findall(r'<script[^>]*>(.*?)</script>', html, _re.DOTALL | _re.IGNORECASE)
|
|
42
|
+
if not scripts:
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
# 只取含环境探测/计算特征的脚本(过滤掉统计/广告等无关脚本)
|
|
46
|
+
challenge_scripts = []
|
|
47
|
+
for s in scripts:
|
|
48
|
+
if _re.search(r'navigator|document\.cookie|btoa|atob|setTimeout|__INITIAL_STATE__|challenge', s, _re.IGNORECASE):
|
|
49
|
+
challenge_scripts.append(s)
|
|
50
|
+
if not challenge_scripts:
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
_skill_dir = Path(__file__).parent.parent / "sub-skills" / "js-run" / "scripts"
|
|
55
|
+
if str(_skill_dir) not in sys.path:
|
|
56
|
+
sys.path.insert(0, str(_skill_dir))
|
|
57
|
+
from jsrun import JsRun
|
|
58
|
+
except (ImportError, OSError):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with JsRun() as jr:
|
|
63
|
+
for script in challenge_scripts:
|
|
64
|
+
try:
|
|
65
|
+
jr.run(script, timeout_ms=3000)
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
# 推进逻辑时间(挑战脚本常用 setTimeout 延迟发通行证)
|
|
69
|
+
jr.advance(5000)
|
|
70
|
+
cookie = jr.get_cookie()
|
|
71
|
+
|
|
72
|
+
if not cookie or len(cookie) < 10:
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
# 用 clearance cookie 重试原请求(curl_cffi Chrome 指纹 + 退避重试)
|
|
76
|
+
from curl_cffi import requests as _cr
|
|
77
|
+
|
|
78
|
+
# 退避墙钟上界:到点不再发起后续尝试、睡眠也不越过(见 docstring)
|
|
79
|
+
_bo_deadline = (_time.monotonic() + backoff_budget_s
|
|
80
|
+
if backoff_budget_s is not None and backoff_budget_s > 0
|
|
81
|
+
else None)
|
|
82
|
+
session = _cr.Session(impersonate="chrome")
|
|
83
|
+
try:
|
|
84
|
+
last_err = ""
|
|
85
|
+
for attempt in range(1, 4):
|
|
86
|
+
if _bo_deadline is not None and _time.monotonic() >= _bo_deadline:
|
|
87
|
+
break
|
|
88
|
+
try:
|
|
89
|
+
resp = session.get(
|
|
90
|
+
url,
|
|
91
|
+
headers={"Cookie": cookie},
|
|
92
|
+
timeout=timeout,
|
|
93
|
+
)
|
|
94
|
+
if resp.status_code == 200 and len(resp.text.strip()) >= 100:
|
|
95
|
+
return {
|
|
96
|
+
"url": url,
|
|
97
|
+
"content": resp.text[:max_chars],
|
|
98
|
+
"html": "",
|
|
99
|
+
"title": "",
|
|
100
|
+
"length": len(resp.text),
|
|
101
|
+
"success": True,
|
|
102
|
+
"error": None,
|
|
103
|
+
"fetch_method": "jsrun_challenge",
|
|
104
|
+
"jsrun_cookie": cookie[:100],
|
|
105
|
+
}
|
|
106
|
+
last_err = f"HTTP {resp.status_code}, {len(resp.text)} 字节"
|
|
107
|
+
except Exception as e:
|
|
108
|
+
last_err = str(e)[:200]
|
|
109
|
+
if attempt < 3:
|
|
110
|
+
_sleep = 5 * attempt # 5s, 10s
|
|
111
|
+
if _bo_deadline is not None:
|
|
112
|
+
_sleep = min(_sleep, _bo_deadline - _time.monotonic())
|
|
113
|
+
if _sleep <= 0:
|
|
114
|
+
break
|
|
115
|
+
_time.sleep(_sleep)
|
|
116
|
+
return {}
|
|
117
|
+
finally:
|
|
118
|
+
# curl 句柄随 Session 持有:MCP 常驻进程里不关会逐次累积
|
|
119
|
+
session.close()
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
return {}
|