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
package/scripts/crawl.py
CHANGED
|
@@ -1,63 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""crawl.py —
|
|
3
|
-
import
|
|
4
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
2
|
+
"""crawl.py — 站点级爬取(fetch_v3 降级链:增强 HTTP → TLS 指纹 → Wayback)"""
|
|
3
|
+
import re, time
|
|
5
4
|
from urllib.parse import urljoin, urlparse
|
|
6
|
-
from
|
|
5
|
+
from bounded_run import run_bounded, TaskError
|
|
6
|
+
from cli_io import dumps
|
|
7
|
+
# fetch_v3 而非裸 urllib fetch:UA 轮换 + Cookie 积累 + curl_cffi 指纹伪造,
|
|
8
|
+
# 反爬站(CF 保护等)从必然失败到大概率成功。批量场景禁浏览器降级
|
|
9
|
+
# (BFS 可能爬几十页,每页启动 Chrome CDP 会拖慢整体)。
|
|
10
|
+
from fetch_v3 import fetch_page_v3 as _fetch_page_v3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _crawl_fetch(url: str, max_chars: int, timeout: int, raw: bool = True) -> dict:
|
|
14
|
+
"""批量爬取用轻量降级链(不升浏览器):HTTP → TLS 指纹 → Wayback。
|
|
15
|
+
|
|
16
|
+
raw 默认 True:爬取需要原始 HTML 提取链接,且 fetch_v3 缓存不存 html,
|
|
17
|
+
必须绕过缓存才能拿到完整页面。通过模块级 fetch_page 转发,
|
|
18
|
+
测试可 monkeypatch crawl.fetch_page 注入假抓取。
|
|
19
|
+
"""
|
|
20
|
+
return fetch_page(url, max_chars=max_chars, timeout=timeout, raw=raw)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# 默认实现指向 fetch_v3 的兼容入口;测试可整体替换此名字
|
|
24
|
+
fetch_page = _fetch_page_v3
|
|
7
25
|
|
|
8
26
|
def crawl_sitemap(url, max_pages=20, timeout=10):
|
|
9
27
|
"""从 sitemap.xml 爬取"""
|
|
28
|
+
t0 = time.monotonic()
|
|
10
29
|
sitemap_url = urljoin(url, '/sitemap.xml')
|
|
11
|
-
result =
|
|
30
|
+
result = _crawl_fetch(sitemap_url, 50000, timeout)
|
|
12
31
|
if not result['success']:
|
|
13
32
|
# 尝试 robots.txt 指定的 sitemap
|
|
14
33
|
robots_url = urljoin(url, '/robots.txt')
|
|
15
|
-
robots_result =
|
|
34
|
+
robots_result = _crawl_fetch(robots_url, 10000, timeout)
|
|
16
35
|
if robots_result['success'] and 'Sitemap:' in robots_result.get('html', ''):
|
|
17
36
|
import re as _re
|
|
18
37
|
sitemap_urls = _re.findall(r'Sitemap:\s*(.+)', robots_result['html'])
|
|
19
38
|
if sitemap_urls:
|
|
20
39
|
sitemap_url = sitemap_urls[0].strip()
|
|
21
|
-
result =
|
|
40
|
+
result = _crawl_fetch(sitemap_url, 50000, timeout)
|
|
22
41
|
if not result.get('success') or not result.get('html'):
|
|
23
42
|
return {'url': url, 'pages': [], 'total': 0, 'error': 'sitemap not found or empty'}
|
|
24
|
-
html = result.get('html'
|
|
25
|
-
# 同时尝试从 raw content 提取(兼容 XML 被 ContentExtractor 过滤的情况)
|
|
43
|
+
html = result.get('html') or result.get('content') or ''
|
|
26
44
|
urls = re.findall(r'<loc>\s*(.*?)\s*</loc>', html)
|
|
27
|
-
if not urls:
|
|
28
|
-
urls = re.findall(r'<loc>(.*?)</loc>', result.get('content', ''))
|
|
29
45
|
urls = urls[:max_pages]
|
|
30
46
|
pages = []
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {'url': url, 'pages': pages, 'total': len(pages), 'elapsed_ms': int((time.
|
|
47
|
+
# 总时限 timeout*2:到点未抓到的站点跳过,不再被卡住的单个 URL 拖住,也不会
|
|
48
|
+
# 因超时异常中断整次 sitemap 爬取。
|
|
49
|
+
finished, _unfinished = run_bounded(
|
|
50
|
+
urls, lambda u: _crawl_fetch(u, 2000, timeout), timeout * 2, max_workers=5)
|
|
51
|
+
for _u, r in finished:
|
|
52
|
+
if isinstance(r, TaskError) or not r.get('success'):
|
|
53
|
+
continue
|
|
54
|
+
pages.append({'url': r['url'], 'content': r['content'][:500], 'depth': 0})
|
|
55
|
+
return {'url': url, 'pages': pages, 'total': len(pages), 'elapsed_ms': int((time.monotonic() - t0) * 1000)}
|
|
40
56
|
|
|
41
57
|
def crawl_bfs(url, max_pages=10, max_depth=2, timeout=8):
|
|
42
|
-
"""BFS
|
|
58
|
+
"""BFS 爬取(按层并行 frontier,去重后入队)"""
|
|
59
|
+
from collections import deque
|
|
43
60
|
visited = set()
|
|
44
61
|
pages = []
|
|
45
|
-
queue = [(url, 0)]
|
|
62
|
+
queue = deque([(url, 0)])
|
|
46
63
|
while queue and len(pages) < max_pages:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
# 取一层(最多 5 个)并行抓取
|
|
65
|
+
frontier = []
|
|
66
|
+
while queue and len(frontier) < 5:
|
|
67
|
+
u, d = queue.popleft()
|
|
68
|
+
if u in visited or d > max_depth:
|
|
69
|
+
continue
|
|
70
|
+
visited.add(u)
|
|
71
|
+
frontier.append((u, d))
|
|
72
|
+
if not frontier:
|
|
73
|
+
break
|
|
74
|
+
# 一层最多 5 个,总时限 timeout*2;到点未完成的 URL 跳过,不被单个慢站拖住。
|
|
75
|
+
finished, _unfinished = run_bounded(
|
|
76
|
+
frontier, lambda ud: _crawl_fetch(ud[0], 2000, timeout),
|
|
77
|
+
timeout * 2, max_workers=min(len(frontier), 5))
|
|
78
|
+
for (u, d), result in finished:
|
|
79
|
+
if isinstance(result, TaskError) or not result.get('success'):
|
|
80
|
+
continue
|
|
81
|
+
pages.append({'url': u, 'content': result['content'][:500], 'depth': d})
|
|
82
|
+
if len(pages) >= max_pages:
|
|
83
|
+
break
|
|
84
|
+
html = result.get('html') or result.get('content') or ''
|
|
85
|
+
links = re.findall(r'href=["\']([^"\'#]+)', html)
|
|
55
86
|
for link in links[:5]:
|
|
56
|
-
full = urljoin(
|
|
57
|
-
if urlparse(full).netloc == urlparse(url).netloc
|
|
58
|
-
|
|
87
|
+
full = urljoin(u, link)
|
|
88
|
+
if (urlparse(full).netloc == urlparse(url).netloc
|
|
89
|
+
and full not in visited):
|
|
90
|
+
queue.append((full, d + 1))
|
|
59
91
|
return {'url': url, 'pages': pages, 'total': len(pages)}
|
|
60
|
-
|
|
61
92
|
if __name__ == '__main__':
|
|
62
93
|
import argparse
|
|
63
94
|
p = argparse.ArgumentParser()
|
|
@@ -65,9 +96,17 @@ if __name__ == '__main__':
|
|
|
65
96
|
p.add_argument('--strategy', default='bfs', choices=['sitemap','bfs'])
|
|
66
97
|
p.add_argument('--max-pages', type=int, default=10)
|
|
67
98
|
p.add_argument('--max-depth', type=int, default=2)
|
|
99
|
+
# bin/argo 的 usage 承诺了 Common flags「--json」,此前 crawl/extract 的
|
|
100
|
+
# parser 里没有它,Agent 照文档传参直接撞墙(error: unrecognized
|
|
101
|
+
# arguments,exit 2)。这里接住该开关并给出真实语义:默认仍是截断预览
|
|
102
|
+
# (防上下文膨胀),--json 输出完整可解析 JSON——截断的 JSON 不是合法
|
|
103
|
+
# JSON,调用方 json.loads 必失败,等于「拿不到数据还得自己猜」。
|
|
104
|
+
p.add_argument('--json', action='store_true',
|
|
105
|
+
help='输出完整 JSON(默认为 2000 字符截断预览)')
|
|
68
106
|
args = p.parse_args()
|
|
69
107
|
if args.strategy == 'sitemap':
|
|
70
108
|
r = crawl_sitemap(args.url, args.max_pages)
|
|
71
109
|
else:
|
|
72
110
|
r = crawl_bfs(args.url, args.max_pages, args.max_depth)
|
|
73
|
-
|
|
111
|
+
out = dumps(r)
|
|
112
|
+
print(out if args.json else out[:2000])
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cross_lingual_trace.py — 跨语言长程搜索与多源任务溯源引擎。
|
|
3
|
+
|
|
4
|
+
架构定位:
|
|
5
|
+
解耦自 research.py 与 wide_research.py。当单语言(如中文或特定小语种)
|
|
6
|
+
信息贫瘠、被低质农场包围,或任务明确要求事实溯源(源头论文、首发漏洞、
|
|
7
|
+
规范草案、最初作者、原始出处)时,自动触发跨语言长程追踪(Pivot & Trace),
|
|
8
|
+
并进行跨语言证据交叉比对(Reconciliation & Disagreement Check)。
|
|
9
|
+
|
|
10
|
+
核心阶段:
|
|
11
|
+
1. 需求与充分性判定(assess_cross_lingual_need):
|
|
12
|
+
检测结果稀疏度、权威源覆盖率以及溯源关键词(起源/出处/最早/first proposed 等)。
|
|
13
|
+
2. 跨语言实体锚点与溯源子查询生成(build_cross_lingual_trace_queries):
|
|
14
|
+
将查询投影至目标语种(以英文为主,或日/德/法垂直语境),生成多阶段溯源子查询。
|
|
15
|
+
3. 跨语言多源证据对账与综合(synthesize_cross_lingual_evidence):
|
|
16
|
+
将跨语言采集回来的结果与原语言结果按一手源等级、时序和互补关系对齐,
|
|
17
|
+
沉淀出 primary_origins(一手源)、complements(互补证据)与 disagreements(口径差异)。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# 溯源与跨语言强触发词
|
|
27
|
+
_TRACING_KEYWORDS = (
|
|
28
|
+
"溯源", "起源", "出处", "最初", "源头", "提出者", "发明", "草案",
|
|
29
|
+
"原型", "首发", "首个", "第一篇", "历史沿革", "来龙去脉",
|
|
30
|
+
"origin", "provenance", "root cause", "first proposed", "author of",
|
|
31
|
+
"initial draft", "whitepaper", "specification", "cve-", "rfc "
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_SOURCE_TYPE_RANKS = {
|
|
35
|
+
"official": 4,
|
|
36
|
+
"academic": 4,
|
|
37
|
+
"standard": 4,
|
|
38
|
+
"repository": 3,
|
|
39
|
+
"reference": 3,
|
|
40
|
+
"news": 2,
|
|
41
|
+
"community": 2,
|
|
42
|
+
"general": 1,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assess_cross_lingual_need(
|
|
47
|
+
query: str,
|
|
48
|
+
results: list[dict[str, Any]] | None,
|
|
49
|
+
domain: str = "",
|
|
50
|
+
min_sufficient_results: int = 3,
|
|
51
|
+
force_trace: bool = False,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""评估是否需要启动跨语言长程搜索与任务溯源。
|
|
54
|
+
|
|
55
|
+
触发条件(满足任意一条):
|
|
56
|
+
1. 显式开启 force_trace 或查询包含溯源关键词;
|
|
57
|
+
2. 结果稀疏(有效结果 < min_sufficient_results);
|
|
58
|
+
3. 高置信度/权威一手信源缺失(authority_level >= 3 的结果为 0);
|
|
59
|
+
4. 强跨语言领域(security_search, academic, package_intel, rfc)。
|
|
60
|
+
"""
|
|
61
|
+
q_low = (query or "").lower()
|
|
62
|
+
has_tracing_keyword = any(kw in q_low for kw in _TRACING_KEYWORDS)
|
|
63
|
+
|
|
64
|
+
valid_results = [r for r in (results or []) if isinstance(r, dict)]
|
|
65
|
+
res_count = len(valid_results)
|
|
66
|
+
|
|
67
|
+
high_auth_count = sum(
|
|
68
|
+
1 for r in valid_results
|
|
69
|
+
if float(r.get("authority") or r.get("score") or 0.0) >= 0.7
|
|
70
|
+
or int(r.get("authority_level") or 0) >= 3
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
domain_low = (domain or "").lower()
|
|
74
|
+
is_cross_lingual_domain = any(
|
|
75
|
+
d in domain_low for d in ("security", "academic", "package", "rfc", "macro", "us_stock")
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
is_sparse = res_count < min_sufficient_results
|
|
79
|
+
no_authoritative = (res_count > 0 and high_auth_count == 0)
|
|
80
|
+
|
|
81
|
+
needed = force_trace or has_tracing_keyword or is_sparse or no_authoritative or (
|
|
82
|
+
is_cross_lingual_domain and high_auth_count < 2
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
reasons: list[str] = []
|
|
86
|
+
if force_trace:
|
|
87
|
+
reasons.append("显式指定跨语言长程溯源")
|
|
88
|
+
if has_tracing_keyword:
|
|
89
|
+
reasons.append("命中溯源意图关键词")
|
|
90
|
+
if is_sparse:
|
|
91
|
+
reasons.append(f"原语言结果不足(仅 {res_count} 条,门槛 {min_sufficient_results})")
|
|
92
|
+
if no_authoritative:
|
|
93
|
+
reasons.append("原语言缺乏高置信度一手权威信源")
|
|
94
|
+
if is_cross_lingual_domain:
|
|
95
|
+
reasons.append(f"领域「{domain}」天然具备跨语言溯源需求")
|
|
96
|
+
|
|
97
|
+
target_langs = ["en"]
|
|
98
|
+
if re.search(r"[\u3040-\u309f\u30a0-\u30ff]", query):
|
|
99
|
+
target_langs = ["en", "zh"]
|
|
100
|
+
elif re.search(r"(日企|日本|动漫|acg|丰田|索尼|任天堂)", query, re.I):
|
|
101
|
+
target_langs = ["en", "ja"]
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"needed": bool(needed),
|
|
105
|
+
"target_langs": target_langs,
|
|
106
|
+
"is_tracing": bool(has_tracing_keyword or force_trace),
|
|
107
|
+
"is_sparse": bool(is_sparse),
|
|
108
|
+
"reasons": reasons,
|
|
109
|
+
"high_auth_count": high_auth_count,
|
|
110
|
+
"result_count": res_count,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract_core_entity(query: str) -> str:
|
|
115
|
+
"""从复杂查询中提取核心溯源实体(英文锚点或关键名词)。"""
|
|
116
|
+
q = (query or "").strip()
|
|
117
|
+
# 优先抽取其中的西文标识(如 CVE-2024-1234, React, Transformer, SQLite 等)
|
|
118
|
+
ascii_matches = re.findall(r"[A-Za-z0-9_\-\.\:\/]{3,}", q)
|
|
119
|
+
if ascii_matches:
|
|
120
|
+
# 过滤掉通用词
|
|
121
|
+
clean_terms = [m for m in ascii_matches if m.lower() not in ("what", "how", "why", "when", "with")]
|
|
122
|
+
if clean_terms:
|
|
123
|
+
return " ".join(clean_terms[:3])
|
|
124
|
+
|
|
125
|
+
# 如果纯 CJK,剥离常见的动词与提问词
|
|
126
|
+
cleaned = re.sub(
|
|
127
|
+
r"(如何|怎么|为什么|溯源|起源|出处|最初|源头|提出者|发明|的来历|是谁|什么时候|最新|评价)",
|
|
128
|
+
" ",
|
|
129
|
+
q,
|
|
130
|
+
).strip()
|
|
131
|
+
words = [w for w in re.split(r"\s+", cleaned) if len(w) >= 2]
|
|
132
|
+
return words[0] if words else q
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_cross_lingual_trace_queries(
|
|
136
|
+
query: str,
|
|
137
|
+
target_lang: str = "en",
|
|
138
|
+
focus: str = "comprehensive",
|
|
139
|
+
) -> list[dict[str, str]]:
|
|
140
|
+
"""生成分阶段跨语言长程溯源子查询。
|
|
141
|
+
|
|
142
|
+
返回结构化的子查询列表,标注意图(intent)、策略(strategy)与语言标记。
|
|
143
|
+
"""
|
|
144
|
+
entity = _extract_core_entity(query)
|
|
145
|
+
subs: list[dict[str, str]] = []
|
|
146
|
+
|
|
147
|
+
if target_lang == "en":
|
|
148
|
+
# 阶段 1:一手原始出处与概念提出(Origin & Inception)
|
|
149
|
+
subs.append({
|
|
150
|
+
"query": f"{entity} original paper first proposed inception origin",
|
|
151
|
+
"intent": "原始论文/源头提出者与发布背景溯源",
|
|
152
|
+
"strategy": "origin_trace",
|
|
153
|
+
"lang": "en",
|
|
154
|
+
})
|
|
155
|
+
# 阶段 2:规范、代码仓与官方权威文档(Specification & Implementation)
|
|
156
|
+
subs.append({
|
|
157
|
+
"query": f"{entity} official repository documentation specification RFC",
|
|
158
|
+
"intent": "官方规范、权威仓库与技术标准追踪",
|
|
159
|
+
"strategy": "primary_spec",
|
|
160
|
+
"lang": "en",
|
|
161
|
+
})
|
|
162
|
+
# 阶段 3:演进考证与关键争议(Evolution & Historical Milestone)
|
|
163
|
+
subs.append({
|
|
164
|
+
"query": f"{entity} milestone evolution history benchmark analysis",
|
|
165
|
+
"intent": "历史演进里程碑与事实交叉考证",
|
|
166
|
+
"strategy": "cross_check",
|
|
167
|
+
"lang": "en",
|
|
168
|
+
})
|
|
169
|
+
elif target_lang == "ja":
|
|
170
|
+
subs.append({
|
|
171
|
+
"query": f"{entity} 開発経緯 歴史 初出 仕様",
|
|
172
|
+
"intent": "日文原始出处与开发历史溯源",
|
|
173
|
+
"strategy": "origin_trace",
|
|
174
|
+
"lang": "ja",
|
|
175
|
+
})
|
|
176
|
+
subs.append({
|
|
177
|
+
"query": f"{entity} 公式ドキュメント 仕様書 論文",
|
|
178
|
+
"intent": "日文官方规范与文档追踪",
|
|
179
|
+
"strategy": "primary_spec",
|
|
180
|
+
"lang": "ja",
|
|
181
|
+
})
|
|
182
|
+
else:
|
|
183
|
+
subs.append({
|
|
184
|
+
"query": f"{entity} origin official source history",
|
|
185
|
+
"intent": f"{target_lang} 语种源头与官方文档追踪",
|
|
186
|
+
"strategy": "origin_trace",
|
|
187
|
+
"lang": target_lang,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return subs
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _classify_source_nature(url: str, title: str) -> str:
|
|
194
|
+
"""根据 URL 和标题对信源进行性质分类。"""
|
|
195
|
+
u_low = (url or "").lower()
|
|
196
|
+
t_low = (title or "").lower()
|
|
197
|
+
|
|
198
|
+
if any(h in u_low for h in ("arxiv.org", "doi.org", "acm.org", "ieee.org", "biorxiv.org", "semanticscholar.org")):
|
|
199
|
+
return "academic"
|
|
200
|
+
if any(h in u_low for h in ("github.com", "gitlab.com", "bitbucket.org", "git.kernel.org")):
|
|
201
|
+
return "repository"
|
|
202
|
+
if any(h in u_low for h in ("rfc-editor.org", "w3.org", "iso.org", "ecma-international.org", "cve.mitre.org", "nvd.nist.gov")):
|
|
203
|
+
return "standard"
|
|
204
|
+
if any(h in u_low for h in (".gov", ".edu", "europa.eu", "who.int", "un.org")):
|
|
205
|
+
return "official"
|
|
206
|
+
if any(h in u_low for h in ("wikipedia.org", "wikidata.org", "baike.baidu.com")):
|
|
207
|
+
return "reference"
|
|
208
|
+
if any(k in t_low for k in ("official", "announcement", "release note", "white paper", "specification")):
|
|
209
|
+
return "official"
|
|
210
|
+
return "general"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def synthesize_cross_lingual_evidence(
|
|
214
|
+
primary_results: list[dict[str, Any]],
|
|
215
|
+
cross_results: list[dict[str, Any]],
|
|
216
|
+
query: str,
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""将原语言与跨语言长程检索证据进行交叉对账。
|
|
219
|
+
|
|
220
|
+
产出:
|
|
221
|
+
- primary_origins: 权威一手信源(论文、代码仓、标准机构)
|
|
222
|
+
- complements: 跨语言带来的增量补充事实
|
|
223
|
+
- disagreements: 跨语言比对中发现的时间/口径分歧
|
|
224
|
+
- provenance_chain: 结构化溯源脉络
|
|
225
|
+
"""
|
|
226
|
+
seen_urls: set[str] = set()
|
|
227
|
+
primary_origins: list[dict[str, Any]] = []
|
|
228
|
+
complements: list[dict[str, Any]] = []
|
|
229
|
+
disagreements: list[str] = []
|
|
230
|
+
|
|
231
|
+
all_cross = [r for r in cross_results if isinstance(r, dict)]
|
|
232
|
+
|
|
233
|
+
# 1. 甄选跨语言一手源
|
|
234
|
+
for r in all_cross:
|
|
235
|
+
u = r.get("url") or ""
|
|
236
|
+
if not u or u in seen_urls:
|
|
237
|
+
continue
|
|
238
|
+
seen_urls.add(u)
|
|
239
|
+
t = r.get("title") or ""
|
|
240
|
+
nature = _classify_source_nature(u, t)
|
|
241
|
+
entry = {
|
|
242
|
+
"title": t,
|
|
243
|
+
"url": u,
|
|
244
|
+
"snippet": r.get("snippet") or "",
|
|
245
|
+
"nature": nature,
|
|
246
|
+
"source": r.get("source") or "",
|
|
247
|
+
"authority_rank": _SOURCE_TYPE_RANKS.get(nature, 1),
|
|
248
|
+
}
|
|
249
|
+
if nature in ("academic", "standard", "official", "repository"):
|
|
250
|
+
primary_origins.append(entry)
|
|
251
|
+
else:
|
|
252
|
+
complements.append(entry)
|
|
253
|
+
|
|
254
|
+
# 排序:权威一手源排在最前
|
|
255
|
+
primary_origins.sort(key=lambda x: x["authority_rank"], reverse=True)
|
|
256
|
+
|
|
257
|
+
# 2. 检查年份/时间戳分歧
|
|
258
|
+
year_re = re.compile(r"\b(19\d{2}|20\d{2})\b")
|
|
259
|
+
prim_years = set()
|
|
260
|
+
for r in primary_results:
|
|
261
|
+
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
262
|
+
prim_years.update(year_re.findall(text))
|
|
263
|
+
|
|
264
|
+
cross_years = set()
|
|
265
|
+
for r in all_cross:
|
|
266
|
+
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
267
|
+
cross_years.update(year_re.findall(text))
|
|
268
|
+
|
|
269
|
+
if prim_years and cross_years and not (prim_years & cross_years):
|
|
270
|
+
disagreements.append(
|
|
271
|
+
f"跨语言时间线存在差异:原语言提及时段 {sorted(prim_years)},而外文一手源提及时段 {sorted(cross_years)}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# 3. 构造溯源链条
|
|
275
|
+
provenance_chain: list[dict[str, str]] = []
|
|
276
|
+
for origin in primary_origins[:4]:
|
|
277
|
+
provenance_chain.append({
|
|
278
|
+
"stage": "一手信源定位",
|
|
279
|
+
"source_type": origin["nature"],
|
|
280
|
+
"title": origin["title"][:80],
|
|
281
|
+
"url": origin["url"],
|
|
282
|
+
"evidence": origin["snippet"][:140],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"status": "completed" if (primary_origins or complements) else "no_incremental_evidence",
|
|
287
|
+
"primary_origins": primary_origins[:6],
|
|
288
|
+
"complements_count": len(complements),
|
|
289
|
+
"complements": complements[:6],
|
|
290
|
+
"disagreements": disagreements,
|
|
291
|
+
"provenance_chain": provenance_chain,
|
|
292
|
+
"total_cross_sources": len(all_cross),
|
|
293
|
+
}
|