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,323 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""browser_auth.py — argo 自持浏览器登录态(持久 profile,A2 车道)
|
|
3
|
+
|
|
4
|
+
落地方案:docs-local/连接器能力接入_方案调研_2026-09-26.md §4 方案 A 的 A2。
|
|
5
|
+
|
|
6
|
+
模型(与既有凭据纪律对齐):
|
|
7
|
+
- profile 是 argo 自持的独立 Chromium profile(不是用户主 Chrome),
|
|
8
|
+
落在状态根 browser-profiles/<site>/。
|
|
9
|
+
- cookie 只存在于 Chrome 自己的存储里(macOS Keychain / Windows DPAPI
|
|
10
|
+
加密),argo 全程不读取、不导出 cookie 值——比导出 state 文件再加密
|
|
11
|
+
(agent-browser --session 的做法)少一类落盘凭据。
|
|
12
|
+
- 抓取走 chrome_cdp 的「页面内带凭证 fetch」(fetch_json):cookie 由
|
|
13
|
+
浏览器附加,argo 只收响应体。
|
|
14
|
+
- 登录态结果标 login_state_used=True / cache_eligible=False,
|
|
15
|
+
cache.assert_cacheable 据此拒绝进公共缓存。
|
|
16
|
+
|
|
17
|
+
CLI(bin/argo 的 auth 子命令):
|
|
18
|
+
argo auth login <url> 开可见窗口登录/扫码一次(Ctrl-C 放弃)
|
|
19
|
+
argo auth status 列出已登录站点
|
|
20
|
+
argo auth logout <site> 删除该站点 profile
|
|
21
|
+
|
|
22
|
+
开关:ARGO_FETCH_AUTH=0 关闭抓取侧登录态车道;auth login/status/logout
|
|
23
|
+
不受它管(用户显式管理自己的 profile 永远可用)。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
ENV_AUTH_FETCH = "ARGO_AUTH_FETCH"
|
|
37
|
+
|
|
38
|
+
# 备名:与抓取降级链分级开关家族(ARGO_FETCH_*)保持同族命名
|
|
39
|
+
ENV_AUTH_FETCH_ALIAS = "ARGO_FETCH_AUTH"
|
|
40
|
+
|
|
41
|
+
_HOST_RE = re.compile(r"[^a-z0-9.-]")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def auth_fetch_enabled() -> bool:
|
|
45
|
+
"""抓取侧登录态车道开关:ARGO_AUTH_FETCH / ARGO_FETCH_AUTH=0 关闭,默认开启。"""
|
|
46
|
+
for name in (ENV_AUTH_FETCH, ENV_AUTH_FETCH_ALIAS):
|
|
47
|
+
try:
|
|
48
|
+
from engine_env import get_env
|
|
49
|
+
val = get_env(name)
|
|
50
|
+
except ImportError:
|
|
51
|
+
val = os.environ.get(name, "")
|
|
52
|
+
if str(val).strip().lower() in ("0", "false", "no", "off"):
|
|
53
|
+
return False
|
|
54
|
+
if str(val).strip():
|
|
55
|
+
return True
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def profile_root() -> Path:
|
|
60
|
+
"""持久 profile 根目录:状态根/browser-profiles(ARGO_STATE_DIR 可整体隔离)。"""
|
|
61
|
+
from argo_paths import state_root
|
|
62
|
+
return state_root() / "browser-profiles"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def site_host(url_or_host: str) -> str:
|
|
66
|
+
"""URL 或主机名 → 站点身份(去 scheme/port/path,去 www.,小写)。
|
|
67
|
+
|
|
68
|
+
返回值直接用作 profile 目录名,所以顺带做文件名安全审查:
|
|
69
|
+
出现 [^a-z0-9.-](路径穿越、空格、怪字符)或没有点(排除裸词)
|
|
70
|
+
一律拒绝。localhost 显式放行(本地开发站登录是真实场景)。
|
|
71
|
+
"""
|
|
72
|
+
s = (url_or_host or "").strip()
|
|
73
|
+
if "://" in s:
|
|
74
|
+
s = s.split("://", 1)[1]
|
|
75
|
+
s = s.split("/", 1)[0].split("?", 1)[0].split("#", 1)[0]
|
|
76
|
+
if s.endswith(":80") or s.endswith(":443"):
|
|
77
|
+
s = s.rsplit(":", 1)[0]
|
|
78
|
+
if ":" in s: # 带端口(或 IPv6 字面量)→ 取端口前段;IPv6 不支持,报错更诚实
|
|
79
|
+
head = s.split(":", 1)[0]
|
|
80
|
+
if not head:
|
|
81
|
+
raise ValueError(f"unsupported site: {url_or_host!r}")
|
|
82
|
+
s = head
|
|
83
|
+
s = s.lower()
|
|
84
|
+
if s.startswith("www."):
|
|
85
|
+
s = s[4:]
|
|
86
|
+
if not s or _HOST_RE.search(s):
|
|
87
|
+
raise ValueError(f"invalid site: {url_or_host!r}")
|
|
88
|
+
if s.startswith("."): # "." / ".." / ".com":点号打头做目录名就是穿越
|
|
89
|
+
raise ValueError(f"invalid site: {url_or_host!r}")
|
|
90
|
+
if "." not in s and s != "localhost":
|
|
91
|
+
raise ValueError(f"invalid site (no dot): {url_or_host!r}")
|
|
92
|
+
return s
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _profile_dir(host: str) -> Path:
|
|
96
|
+
return profile_root() / host
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_meta(d: Path) -> dict:
|
|
100
|
+
try:
|
|
101
|
+
return json.loads((d / "meta.json").read_text(encoding="utf-8"))
|
|
102
|
+
except Exception:
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _write_meta(d: Path, host: str, created: str | None = None) -> None:
|
|
107
|
+
"""原子写 meta.json:profile 的 argo 登记标记(Chrome 不认识也不在乎它)。"""
|
|
108
|
+
prev = _read_meta(d)
|
|
109
|
+
meta = {
|
|
110
|
+
"site": host,
|
|
111
|
+
"created": created or prev.get("created") or time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
112
|
+
"last_used": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
113
|
+
"argo": "browser-profiles/v1",
|
|
114
|
+
}
|
|
115
|
+
tmp = d / f"meta.json.tmp.{os.getpid()}"
|
|
116
|
+
tmp.write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")
|
|
117
|
+
os.replace(tmp, d / "meta.json")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def profile_for(url_or_host: str) -> str | None:
|
|
121
|
+
"""该站点的持久 profile 路径;没登录过(或站点名不合法)返回 None。
|
|
122
|
+
|
|
123
|
+
以 meta.json 存在为准:裸目录可能是上次登录失败的残骸,不能当登录态。
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
host = site_host(url_or_host)
|
|
127
|
+
except ValueError:
|
|
128
|
+
return None
|
|
129
|
+
d = _profile_dir(host)
|
|
130
|
+
if d.is_dir() and (d / "meta.json").is_file():
|
|
131
|
+
return str(d)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def auth_profile_for_fetch(url_or_host: str,
|
|
136
|
+
allow_browser_lane: bool = True) -> str | None:
|
|
137
|
+
"""抓取侧入口:车道让路(开关关 / 调用方禁浏览器 / 未登录)→ None。
|
|
138
|
+
|
|
139
|
+
allow_browser_lane=False 对应 use_browser_fallback=False 的调用方
|
|
140
|
+
(批量爬取、evidence 核验等刻意无浏览器的路径)——登录态车道同样让路,
|
|
141
|
+
未认证不破默认路径的另一半。
|
|
142
|
+
"""
|
|
143
|
+
if not allow_browser_lane or not auth_fetch_enabled():
|
|
144
|
+
return None
|
|
145
|
+
return profile_for(url_or_host)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def login(url_or_host: str, chrome_path: str | None = None,
|
|
149
|
+
confirm=None) -> dict:
|
|
150
|
+
"""开可见 Chrome 窗口让用户登录一次,profile 持久保存。
|
|
151
|
+
|
|
152
|
+
confirm:等待登录完成的钩子(None → 终端 input())。可注入以便测试。
|
|
153
|
+
"""
|
|
154
|
+
host = site_host(url_or_host)
|
|
155
|
+
d = _profile_dir(host)
|
|
156
|
+
fresh = not d.is_dir()
|
|
157
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
target = url_or_host.strip()
|
|
160
|
+
if "://" not in target:
|
|
161
|
+
target = "https://" + target
|
|
162
|
+
|
|
163
|
+
from chrome_cdp import _ChromeProcess
|
|
164
|
+
proc = _ChromeProcess(chrome_path=chrome_path, headless=False,
|
|
165
|
+
user_data_dir=str(d), start_url=target)
|
|
166
|
+
try:
|
|
167
|
+
proc.start()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
# 新建 profile 时启动即败(占位 SingletonLock / 磁盘满等)→ 别留裸目录
|
|
170
|
+
if fresh:
|
|
171
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
172
|
+
msg = str(e)
|
|
173
|
+
if "CDP failed to start" in msg or "Singleton" in msg:
|
|
174
|
+
msg += "(该 profile 可能已有 Chrome 在跑:关掉它或先 argo auth logout)"
|
|
175
|
+
return {"ok": False, "site": host, "error": msg[:200]}
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
if confirm is None:
|
|
179
|
+
print(f"\n[{host}] 已在打开的 Chrome 窗口中登录/扫码。", file=sys.stderr)
|
|
180
|
+
print("完成后回到终端按 Enter 保存登录态(Ctrl-C 放弃):", file=sys.stderr)
|
|
181
|
+
try:
|
|
182
|
+
input()
|
|
183
|
+
except EOFError:
|
|
184
|
+
return {"ok": False, "site": host,
|
|
185
|
+
"error": "非交互终端无法确认登录完成(需要 tty)"}
|
|
186
|
+
else:
|
|
187
|
+
confirm()
|
|
188
|
+
_write_meta(d, host)
|
|
189
|
+
return {"ok": True, "site": host, "profile": str(d)}
|
|
190
|
+
finally:
|
|
191
|
+
try:
|
|
192
|
+
proc.stop()
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def status() -> list[dict]:
|
|
198
|
+
"""已登录站点清单(只读 meta,绝不触碰 profile 内的浏览器存储)。"""
|
|
199
|
+
root = profile_root()
|
|
200
|
+
if not root.is_dir():
|
|
201
|
+
return []
|
|
202
|
+
out = []
|
|
203
|
+
for d in sorted(root.iterdir()):
|
|
204
|
+
if not (d.is_dir() and (d / "meta.json").is_file()):
|
|
205
|
+
continue
|
|
206
|
+
m = _read_meta(d)
|
|
207
|
+
out.append({
|
|
208
|
+
"site": m.get("site") or d.name,
|
|
209
|
+
"created": m.get("created"),
|
|
210
|
+
"last_used": m.get("last_used"),
|
|
211
|
+
"path": str(d),
|
|
212
|
+
})
|
|
213
|
+
return out
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def logout(url_or_host: str) -> dict:
|
|
217
|
+
"""删除站点 profile。Chrome 还开着该 profile 时拒绝(SingletonLock 探活)。"""
|
|
218
|
+
host = site_host(url_or_host)
|
|
219
|
+
d = _profile_dir(host)
|
|
220
|
+
if not d.is_dir():
|
|
221
|
+
return {"ok": False, "site": host, "error": f"no profile for {host}"}
|
|
222
|
+
lock = d / "SingletonLock"
|
|
223
|
+
if lock.is_symlink():
|
|
224
|
+
# Chrome 的 SingletonLock 是 hostname-PID 软链;PID 活着 → 别删活人脚下的地毯
|
|
225
|
+
pid = os.readlink(lock).rsplit("-", 1)[-1]
|
|
226
|
+
if pid.isdigit():
|
|
227
|
+
try:
|
|
228
|
+
os.kill(int(pid), 0)
|
|
229
|
+
except OSError:
|
|
230
|
+
pass # 进程已死:锁是残骸,放行删除
|
|
231
|
+
else:
|
|
232
|
+
return {"ok": False, "site": host,
|
|
233
|
+
"error": f"Chrome(pid {pid}) 正在使用该 profile,先关闭再 logout"}
|
|
234
|
+
shutil.rmtree(d)
|
|
235
|
+
return {"ok": True, "site": host}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def touch(url_or_host: str) -> None:
|
|
239
|
+
"""抓取命中登录态车道时更新 last_used(失败静默——只是观测字段)。"""
|
|
240
|
+
try:
|
|
241
|
+
host = site_host(url_or_host)
|
|
242
|
+
d = _profile_dir(host)
|
|
243
|
+
if d.is_dir() and (d / "meta.json").is_file():
|
|
244
|
+
_write_meta(d, host)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ─── CLI(bin/argo 的 auth 子命令入口)──────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
def main(argv: list[str] | None = None) -> None:
|
|
252
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
253
|
+
use_json = "--json" in argv
|
|
254
|
+
argv = [a for a in argv if a != "--json"]
|
|
255
|
+
|
|
256
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
257
|
+
print(__doc__.strip() or "argo auth — 浏览器登录态")
|
|
258
|
+
sys.exit(0 if argv else 1)
|
|
259
|
+
|
|
260
|
+
cmd, rest = argv[0], argv[1:]
|
|
261
|
+
|
|
262
|
+
def _emit_json(payload: dict) -> None:
|
|
263
|
+
# stdout 一律紧凑 JSON(context-budget 门禁:禁缩进 JSON / dumps_pretty)
|
|
264
|
+
try:
|
|
265
|
+
from cli_io import dumps
|
|
266
|
+
print(dumps(payload))
|
|
267
|
+
except ImportError:
|
|
268
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
269
|
+
|
|
270
|
+
if cmd == "login":
|
|
271
|
+
if not rest:
|
|
272
|
+
print("用法:argo auth login <url>(如 argo auth login https://example.com)",
|
|
273
|
+
file=sys.stderr)
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
chrome_path = None
|
|
276
|
+
if "--chrome-path" in rest:
|
|
277
|
+
i = rest.index("--chrome-path")
|
|
278
|
+
if i + 1 < len(rest):
|
|
279
|
+
chrome_path = rest[i + 1]
|
|
280
|
+
del rest[i:i + 2]
|
|
281
|
+
out = login(rest[0], chrome_path=chrome_path)
|
|
282
|
+
if use_json:
|
|
283
|
+
_emit_json(out)
|
|
284
|
+
elif out.get("ok"):
|
|
285
|
+
print(f"已保存 {out['site']} 的登录态({out['profile']})\n"
|
|
286
|
+
f"之后 argo fetch 该站点自动带会话;argo auth status 可查看。")
|
|
287
|
+
else:
|
|
288
|
+
print(f"登录失败:{out.get('error')}", file=sys.stderr)
|
|
289
|
+
sys.exit(0 if out.get("ok") else 1)
|
|
290
|
+
|
|
291
|
+
if cmd == "status":
|
|
292
|
+
profiles = status()
|
|
293
|
+
if use_json:
|
|
294
|
+
_emit_json({"profiles": profiles})
|
|
295
|
+
elif not profiles:
|
|
296
|
+
print("(尚无已登录站点——argo auth login <url> 添加)")
|
|
297
|
+
else:
|
|
298
|
+
for p in profiles:
|
|
299
|
+
print(f"{p['site']} 建于 {p.get('created') or '?'} "
|
|
300
|
+
f"最近使用 {p.get('last_used') or '?'}")
|
|
301
|
+
sys.exit(0)
|
|
302
|
+
|
|
303
|
+
if cmd == "logout":
|
|
304
|
+
if not rest:
|
|
305
|
+
print("用法:argo auth logout <site>(站点见 argo auth status)",
|
|
306
|
+
file=sys.stderr)
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
out = logout(rest[0])
|
|
309
|
+
if use_json:
|
|
310
|
+
_emit_json(out)
|
|
311
|
+
elif out.get("ok"):
|
|
312
|
+
print(f"已删除 {out['site']} 的登录态 profile。")
|
|
313
|
+
else:
|
|
314
|
+
print(f"删除失败:{out.get('error')}", file=sys.stderr)
|
|
315
|
+
sys.exit(0 if out.get("ok") else 1)
|
|
316
|
+
|
|
317
|
+
print(f"Unknown auth subcommand: {cmd}(可用:login / status / logout)",
|
|
318
|
+
file=sys.stderr)
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""burstiness_detector.py — 突发性检测器
|
|
3
|
+
|
|
4
|
+
基于 2026 年论文《Ratio of Quantiles Indicates Burstiness with Fewer False
|
|
5
|
+
Negatives than the Conventional Burstiness Parameter》的突发性量化方法,
|
|
6
|
+
检测文本节奏的自然度。
|
|
7
|
+
|
|
8
|
+
核心思想:
|
|
9
|
+
人类写作有「突发性」——长短句交替、节奏变化;AI 文本则过于平稳。
|
|
10
|
+
论文提出用分位数比(Quantile Ratio)替代传统突发性参数,减少假阴性。
|
|
11
|
+
|
|
12
|
+
自然文本的分位数比通常在 1.5-4.0 之间:
|
|
13
|
+
- < 1.5: 过于平稳(可能是 AI 生成)
|
|
14
|
+
- 1.5-4.0: 自然节奏
|
|
15
|
+
- > 4.0: 过于跳跃(可能是拼接内容)
|
|
16
|
+
|
|
17
|
+
设计原则:
|
|
18
|
+
- 纯 stdlib,零依赖,微秒级
|
|
19
|
+
- 与 stylometry_detector 互补:后者看词汇/结构,本模块看节奏
|
|
20
|
+
- 输出 0-1 分数,越高越不自然
|
|
21
|
+
|
|
22
|
+
参考文献:
|
|
23
|
+
- "Ratio of Quantiles Indicates Burstiness with Fewer False Negatives
|
|
24
|
+
than the Conventional Burstiness Parameter" (2026)
|
|
25
|
+
- "The Limitations of Stylometry for Detecting Machine-Generated
|
|
26
|
+
Fake News" EMNLP 2019
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 句子分割模式
|
|
35
|
+
_SENTENCE_RE = re.compile(r'[.!?。!?]+')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _split_sentences(text: str) -> list[str]:
|
|
39
|
+
"""分割句子"""
|
|
40
|
+
if not text:
|
|
41
|
+
return []
|
|
42
|
+
parts = _SENTENCE_RE.split(text)
|
|
43
|
+
return [p.strip() for p in parts if p.strip()]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _quantile(data: list[float], q: float) -> float:
|
|
47
|
+
"""计算分位数"""
|
|
48
|
+
if not data:
|
|
49
|
+
return 0.0
|
|
50
|
+
sorted_data = sorted(data)
|
|
51
|
+
idx = int(len(sorted_data) * q)
|
|
52
|
+
return sorted_data[min(idx, len(sorted_data) - 1)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BurstinessDetector:
|
|
56
|
+
"""突发性检测器 — 量化文本节奏自然度"""
|
|
57
|
+
|
|
58
|
+
def compute_burstiness(self, text: str) -> dict[str, Any]:
|
|
59
|
+
"""计算文本突发性指标
|
|
60
|
+
|
|
61
|
+
返回:
|
|
62
|
+
burstiness_ratio: 分位数比(Q75/Q25)
|
|
63
|
+
is_natural: 是否自然(1.5 <= ratio <= 4.0)
|
|
64
|
+
q25: 25% 分位数
|
|
65
|
+
q75: 75% 分位数
|
|
66
|
+
sentence_count: 句子数
|
|
67
|
+
mean_length: 平均句长
|
|
68
|
+
"""
|
|
69
|
+
sentences = _split_sentences(text)
|
|
70
|
+
if len(sentences) < 3:
|
|
71
|
+
return {
|
|
72
|
+
"burstiness_ratio": 0.0,
|
|
73
|
+
"is_natural": False,
|
|
74
|
+
"q25": 0.0,
|
|
75
|
+
"q75": 0.0,
|
|
76
|
+
"sentence_count": len(sentences),
|
|
77
|
+
"mean_length": 0.0,
|
|
78
|
+
"note": "too_few_sentences",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lengths = [len(s) for s in sentences]
|
|
82
|
+
q25 = _quantile(lengths, 0.25)
|
|
83
|
+
q75 = _quantile(lengths, 0.75)
|
|
84
|
+
|
|
85
|
+
# 分位数比(论文方法):比传统方差更鲁棒
|
|
86
|
+
burstiness_ratio = q75 / max(q25, 1)
|
|
87
|
+
|
|
88
|
+
# 自然文本通常有 1.2-15.0 的分位数比(放宽范围,减少假阴性)
|
|
89
|
+
# 中文文本的 ratio 可能更高(短句+长句对比强烈)
|
|
90
|
+
is_natural = 1.2 <= burstiness_ratio <= 15.0
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"burstiness_ratio": round(burstiness_ratio, 2),
|
|
94
|
+
"is_natural": is_natural,
|
|
95
|
+
"q25": round(q25, 1),
|
|
96
|
+
"q75": round(q75, 1),
|
|
97
|
+
"sentence_count": len(sentences),
|
|
98
|
+
"mean_length": round(sum(lengths) / len(lengths), 1),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def score(self, text: str) -> dict[str, Any]:
|
|
102
|
+
"""计算节奏异常分数
|
|
103
|
+
|
|
104
|
+
返回:
|
|
105
|
+
score: 0-1,越高越不自然
|
|
106
|
+
burstiness: 原始突发性指标
|
|
107
|
+
is_suspicious: 是否可疑(score > 0.6)
|
|
108
|
+
"""
|
|
109
|
+
burstiness = self.compute_burstiness(text)
|
|
110
|
+
|
|
111
|
+
if burstiness.get("note") == "too_few_sentences":
|
|
112
|
+
return {
|
|
113
|
+
"score": 0.5, # 句子太少,无法判断,给中等分数
|
|
114
|
+
"burstiness": burstiness,
|
|
115
|
+
"is_suspicious": False,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ratio = burstiness["burstiness_ratio"]
|
|
119
|
+
|
|
120
|
+
# 使用对数尺度计算异常分数
|
|
121
|
+
# ratio=1.0(完全平稳)→ score=1.0
|
|
122
|
+
# ratio=2.0(自然)→ score=0.0
|
|
123
|
+
# ratio=11.0(中文自然)→ score=0.0
|
|
124
|
+
import math
|
|
125
|
+
if ratio <= 1.0:
|
|
126
|
+
score = 1.0
|
|
127
|
+
elif ratio <= 2.0:
|
|
128
|
+
# 1.0 < ratio <= 2.0: 线性从 1.0 降到 0.0
|
|
129
|
+
score = 2.0 - ratio
|
|
130
|
+
elif ratio <= 10.0:
|
|
131
|
+
# 2.0 < ratio <= 10.0: 自然范围
|
|
132
|
+
score = 0.0
|
|
133
|
+
else:
|
|
134
|
+
# ratio > 10.0: 过于跳跃
|
|
135
|
+
score = min((ratio - 10.0) / 10.0, 1.0)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"score": round(score, 3),
|
|
139
|
+
"burstiness": burstiness,
|
|
140
|
+
"is_suspicious": score > 0.6,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def detect_unnatural_rhythm(text: str) -> dict[str, Any]:
|
|
145
|
+
"""便捷函数:检测文本节奏是否不自然"""
|
|
146
|
+
detector = BurstinessDetector()
|
|
147
|
+
return detector.score(text)
|