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,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""免 key(keyless)通道构建器:官方匿名额度源。
|
|
3
|
+
|
|
4
|
+
与计费通道分开注册的理由:同上游、不同经济模型。匿名额度不读 key,
|
|
5
|
+
按 IP/匿名池限流;与账号档混在一个引擎里会让配额与成本记账失真
|
|
6
|
+
(与 parallel / parallel_free 的既有处理一致)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from engines_base import http_open, safe_search
|
|
17
|
+
|
|
18
|
+
from engines_builders_tech import _mcp_error_of, _parse_mcp_wire
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Keenable 免 key 通道(官方 MCP 匿名额度)────────────────────────────────
|
|
22
|
+
|
|
23
|
+
_KEENABLE_MCP_URL = "https://api.keenable.ai/mcp"
|
|
24
|
+
_KEENABLE_BLOCK_RE = re.compile(r"\n\s*-{3,}\s*\n")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _keenable_block_to_result(block: str) -> dict[str, Any] | None:
|
|
28
|
+
"""Keenable MCP 的纯文本块 → 统一结果。字段以行首标签分隔。"""
|
|
29
|
+
text = (block or "").strip()
|
|
30
|
+
if not text:
|
|
31
|
+
return None
|
|
32
|
+
title = re.search(r"(?m)^Title:\s*(.+?)\s*$", text)
|
|
33
|
+
url = re.search(r"(?m)^URL:\s*(\S+)\s*$", text)
|
|
34
|
+
if not title or not url:
|
|
35
|
+
return None
|
|
36
|
+
published = re.search(r"(?m)^Published:\s*(\S+)", text)
|
|
37
|
+
body = ""
|
|
38
|
+
m = re.search(r"(?ms)^Snippets:\s*\n?(.*)$", text)
|
|
39
|
+
if m:
|
|
40
|
+
body = m.group(1)
|
|
41
|
+
body = re.sub(r"\[\.\.\.\]", " ", body)
|
|
42
|
+
body = re.sub(r"\s+", " ", body).strip()
|
|
43
|
+
row: dict[str, Any] = {
|
|
44
|
+
"title": title.group(1).strip()[:200],
|
|
45
|
+
"url": url.group(1).strip(),
|
|
46
|
+
"snippet": body[:300],
|
|
47
|
+
"source": "keenable_free",
|
|
48
|
+
}
|
|
49
|
+
if published:
|
|
50
|
+
row["published_at"] = published.group(1).strip()[:32]
|
|
51
|
+
return row
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _build_keenable_free_engine(spec: dict[str, Any]) -> Any:
|
|
55
|
+
"""Keenable 免 key:POST 官方 MCP(tools/call search_web_pages)。
|
|
56
|
+
|
|
57
|
+
为什么单独成引擎而不是让 keenable 回退:与 tavily_free / parallel_free
|
|
58
|
+
同一条口径——keyless 是匿名额度通道,与按 credits 计费的 REST 通道
|
|
59
|
+
经济模型不同,合进一个引擎会让配额与成本记账混在一起。
|
|
60
|
+
|
|
61
|
+
上游返回纯文本块(Title/URL/Published/Snippets,`---` 分隔),不是
|
|
62
|
+
JSON;解析失败按「带 error 的记录」上报,让调度层知道是通道/格式问题,
|
|
63
|
+
而不是「网上没有」。
|
|
64
|
+
"""
|
|
65
|
+
timeout = spec.get("timeout", 20)
|
|
66
|
+
|
|
67
|
+
@safe_search
|
|
68
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
69
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
70
|
+
to = _timeout or timeout
|
|
71
|
+
body = json.dumps({
|
|
72
|
+
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
73
|
+
"params": {"name": "search_web_pages",
|
|
74
|
+
"arguments": {"query": query, "mode": "realtime"}},
|
|
75
|
+
}).encode("utf-8")
|
|
76
|
+
headers = {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
"Accept": "application/json, text/event-stream",
|
|
79
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
80
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
81
|
+
"Chrome/120.0.0.0 Safari/537.36",
|
|
82
|
+
}
|
|
83
|
+
req = urllib.request.Request(_KEENABLE_MCP_URL, data=body, headers=headers)
|
|
84
|
+
try:
|
|
85
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
86
|
+
raw = resp.read().decode("utf-8", "replace")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return [{"error": f"keenable_free mcp: {type(e).__name__}: {e}",
|
|
89
|
+
"source": "keenable_free"}]
|
|
90
|
+
data = _parse_mcp_wire(raw)
|
|
91
|
+
if not isinstance(data, dict):
|
|
92
|
+
return [{"error": "keenable_free: 响应不是可解析的 MCP 应答",
|
|
93
|
+
"source": "keenable_free"}]
|
|
94
|
+
mcp_err = _mcp_error_of(data)
|
|
95
|
+
if mcp_err:
|
|
96
|
+
return [{"error": mcp_err, "source": "keenable_free"}]
|
|
97
|
+
content = ((data.get("result") or {}).get("content") or [])
|
|
98
|
+
text = "".join(str(b.get("text") or "") for b in content
|
|
99
|
+
if isinstance(b, dict))
|
|
100
|
+
if not text.strip():
|
|
101
|
+
return []
|
|
102
|
+
out: list[dict[str, Any]] = []
|
|
103
|
+
for block in _KEENABLE_BLOCK_RE.split("\n" + text):
|
|
104
|
+
row = _keenable_block_to_result(block)
|
|
105
|
+
if row:
|
|
106
|
+
out.append(row)
|
|
107
|
+
if len(out) >= max(1, int(n)):
|
|
108
|
+
break
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
return _engine
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""专用构建器:通用搜索 API(parallel.ai / you.com)——按官方文档保持一致
|
|
3
|
+
|
|
4
|
+
parallel(docs.parallel.ai/search/search-quickstart):
|
|
5
|
+
- objective(自然语言目标)+ search_queries(数组)为官方推荐组合
|
|
6
|
+
- mode: turbo(~200ms, $1/千次, 仅英日文) / basic(~1s, $5/千次) / advanced(~3s, $5/千次)
|
|
7
|
+
argo 默认 basic,deep 模式 advanced
|
|
8
|
+
- advanced_settings: excerpt_settings.max_chars_per_result 与 argo snippet 截断保持一致
|
|
9
|
+
|
|
10
|
+
you.com(docs.you.com):
|
|
11
|
+
- 官方环境变量名 YDC_API_KEY
|
|
12
|
+
- freshness(day/week/month/year)按查询时效敏感度动态化(同 bocha 逻辑)
|
|
13
|
+
- language 按查询主语言下推(zh/en)
|
|
14
|
+
- page_age 为 ISO 8601 时间戳 → 提取为 published_at
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.request
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from engines_base import safe_search, http_open, mcp_error_of as _mcp_error_of
|
|
27
|
+
from engine_env import get_env
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _LazyLogger:
|
|
31
|
+
"""延迟创建 logger,避免 import logging 的重量级 import 链(traceback→dataclasses→inspect)。"""
|
|
32
|
+
_logger = None
|
|
33
|
+
|
|
34
|
+
def __getattr__(self, name: str):
|
|
35
|
+
if _LazyLogger._logger is None:
|
|
36
|
+
import logging
|
|
37
|
+
_LazyLogger._logger = logging.getLogger("unified_search.engines")
|
|
38
|
+
if not _LazyLogger._logger.handlers:
|
|
39
|
+
_LazyLogger._logger.setLevel(logging.WARNING)
|
|
40
|
+
_LazyLogger._logger.addHandler(logging.StreamHandler(sys.stderr))
|
|
41
|
+
return getattr(_LazyLogger._logger, name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
logger = _LazyLogger()
|
|
45
|
+
|
|
46
|
+
# ── Parallel 搜索(api.parallel.ai)────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
_PARALLEL_URL = "https://api.parallel.ai/v1/search"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parallel_key() -> str:
|
|
52
|
+
# get_env 候选链(2026-09-29,issue #12 同类):文档推 ARGO_ 前缀名、
|
|
53
|
+
# 老集成留裸名,两套都必须认。此前的裸 os.environ.get 只认裸名——
|
|
54
|
+
# 用户按文档配好 ARGO_PARALLEL_API_KEY 后状态层报 ready、执行层取到
|
|
55
|
+
# 空串,恒 0 结果且失败被 L2 负缓存回放(见 cache_guard 失败态守卫)。
|
|
56
|
+
return get_env(["ARGO_PARALLEL_API_KEY", "PARALLEL_API_KEY"])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parallel_mode(query: str, depth: str) -> str:
|
|
60
|
+
"""mode 选择:deep → advanced;中文查询不可用 turbo(官方仅英日文)→ basic;英文 fast/auto → basic。"""
|
|
61
|
+
if depth == "deep":
|
|
62
|
+
return "advanced"
|
|
63
|
+
return "basic"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_parallel_engine(spec: dict[str, Any]) -> Any:
|
|
67
|
+
"""Parallel Search:objective + search_queries + mode(官方推荐组合)。"""
|
|
68
|
+
timeout = spec.get("timeout", 15)
|
|
69
|
+
|
|
70
|
+
@safe_search
|
|
71
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
72
|
+
depth: str = "fast", **kwargs) -> list[dict[str, Any]]:
|
|
73
|
+
to = _timeout or timeout
|
|
74
|
+
key = _parallel_key()
|
|
75
|
+
if not key:
|
|
76
|
+
return [{"error": "PARALLEL_API_KEY 未设置", "source": "parallel"}]
|
|
77
|
+
limit = max(1, int(n or 5))
|
|
78
|
+
# 多路召回:原查询 + 无 LLM 变体(问句化/概念扩展等),官方最佳实践 2-3 个
|
|
79
|
+
try:
|
|
80
|
+
from query_variants import generate_query_variations
|
|
81
|
+
queries = generate_query_variations(query)[:3] or [query]
|
|
82
|
+
except Exception:
|
|
83
|
+
queries = [query]
|
|
84
|
+
body = {
|
|
85
|
+
"objective": f"Find the latest, most relevant information about: {query}",
|
|
86
|
+
"search_queries": queries,
|
|
87
|
+
"mode": _parallel_mode(query, depth),
|
|
88
|
+
"advanced_settings": {
|
|
89
|
+
"max_results": min(limit, 20),
|
|
90
|
+
"excerpt_settings": {"max_chars_per_result": 300},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
# argo 时间窗 --since → after_date(官方 --after-date)
|
|
94
|
+
since = kwargs.get("since")
|
|
95
|
+
if since:
|
|
96
|
+
date_str = str(since)[:10]
|
|
97
|
+
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", date_str):
|
|
98
|
+
body["advanced_settings"]["source_policy"] = {"after_date": date_str}
|
|
99
|
+
req = urllib.request.Request(
|
|
100
|
+
_PARALLEL_URL,
|
|
101
|
+
data=json.dumps(body).encode("utf-8"),
|
|
102
|
+
headers={"X-Api-Key": key, "Content-Type": "application/json"},
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
106
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"parallel 请求失败: {e}")
|
|
109
|
+
return []
|
|
110
|
+
results: list[dict[str, Any]] = []
|
|
111
|
+
for item in (data.get("results") or [])[:limit]:
|
|
112
|
+
title = str(item.get("title") or "")[:200]
|
|
113
|
+
url = str(item.get("url") or "")
|
|
114
|
+
if not (title or url):
|
|
115
|
+
continue
|
|
116
|
+
excerpts = item.get("excerpts") or []
|
|
117
|
+
snippet = str(excerpts[0])[:300] if excerpts else ""
|
|
118
|
+
results.append({
|
|
119
|
+
"title": title,
|
|
120
|
+
"url": url,
|
|
121
|
+
"snippet": snippet,
|
|
122
|
+
"source": "parallel",
|
|
123
|
+
"published_at": str(item.get("publish_date") or "")[:64],
|
|
124
|
+
})
|
|
125
|
+
return results
|
|
126
|
+
return _engine
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── Parallel 免费通道(search.parallel.ai MCP,keyless)───────────────────────
|
|
130
|
+
|
|
131
|
+
_PARALLEL_FREE_MCP_URL = "https://search.parallel.ai/mcp"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_parallel_free_engine(spec: dict[str, Any]) -> Any:
|
|
135
|
+
"""Parallel 免费搜索:官方免费 MCP 端点的 web_search 工具,恒走免 key 通道。
|
|
136
|
+
|
|
137
|
+
与 parallel(REST + PARALLEL_API_KEY,按量计费)同上游、不同经济模型,
|
|
138
|
+
故注册为独立引擎而非共用 cost_tier——「计费源不当免费」检查的语义
|
|
139
|
+
对两条通道各自成立。本引擎**不看 key**:key 的有无/有效与否是 parallel
|
|
140
|
+
的事;免费端点不收 key、按 key 计费,带了反而可能混淆计费归属。
|
|
141
|
+
路由上以 daily_support 低优先级做补位:每家族 max_per_family=2 挡住
|
|
142
|
+
与 parallel 的日常同查冗余,计费通道缺位(额度耗尽/失败/未配 key)时
|
|
143
|
+
由本通道接住。
|
|
144
|
+
2026-09-14 实测:无状态直调成立(免 initialize/session),响应
|
|
145
|
+
content[0].text 内嵌 JSON,字段与 REST 响应同构(url/title/publish_date/
|
|
146
|
+
excerpts);响应 _meta.parallel.usage 自带上游计量(sku_search,免费
|
|
147
|
+
通道 cost_usd 仅记账)。
|
|
148
|
+
"""
|
|
149
|
+
timeout = spec.get("timeout", 15)
|
|
150
|
+
|
|
151
|
+
@safe_search
|
|
152
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
153
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
154
|
+
to = _timeout or timeout
|
|
155
|
+
limit = max(1, int(n or 5))
|
|
156
|
+
# 多路召回:同 REST 通道,官方最佳实践 2-3 个关键词探针
|
|
157
|
+
try:
|
|
158
|
+
from query_variants import generate_query_variations
|
|
159
|
+
queries = generate_query_variations(query)[:3] or [query]
|
|
160
|
+
except Exception:
|
|
161
|
+
queries = [query]
|
|
162
|
+
body = {
|
|
163
|
+
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
164
|
+
"params": {"name": "web_search", "arguments": {
|
|
165
|
+
"objective": f"Find the latest, most relevant information about: {query}",
|
|
166
|
+
"search_queries": queries,
|
|
167
|
+
}},
|
|
168
|
+
}
|
|
169
|
+
try:
|
|
170
|
+
from http_client import HttpClient
|
|
171
|
+
# max_retries=0:引擎内不做连接级重试(同 anysearch,防与调度层重试叠乘)
|
|
172
|
+
client = HttpClient(timeout=to, max_retries=0, jitter=False)
|
|
173
|
+
resp = client.post(_PARALLEL_FREE_MCP_URL, body=body,
|
|
174
|
+
extra_headers={"Content-Type": "application/json"})
|
|
175
|
+
except ImportError:
|
|
176
|
+
return []
|
|
177
|
+
if resp.get("status", 0) >= 400 or not resp.get("text"):
|
|
178
|
+
return []
|
|
179
|
+
try:
|
|
180
|
+
data = json.loads(resp["text"])
|
|
181
|
+
except (ValueError, TypeError):
|
|
182
|
+
return []
|
|
183
|
+
mcp_err = _mcp_error_of(data, source="parallel_free")
|
|
184
|
+
if mcp_err:
|
|
185
|
+
logger.warning(f"parallel_free 上游错误: {mcp_err}")
|
|
186
|
+
return [{"error": mcp_err, "source": "parallel_free"}]
|
|
187
|
+
inner: dict[str, Any] = {}
|
|
188
|
+
for block in (data.get("result", {}).get("content") or []):
|
|
189
|
+
text = block.get("text", "") if isinstance(block, dict) else str(block)
|
|
190
|
+
try:
|
|
191
|
+
parsed = json.loads(text)
|
|
192
|
+
except (ValueError, TypeError):
|
|
193
|
+
continue
|
|
194
|
+
if isinstance(parsed, dict) and parsed.get("results"):
|
|
195
|
+
inner = parsed
|
|
196
|
+
break
|
|
197
|
+
results: list[dict[str, Any]] = []
|
|
198
|
+
for item in (inner.get("results") or [])[:limit]:
|
|
199
|
+
title = str(item.get("title") or "")[:200]
|
|
200
|
+
url = str(item.get("url") or "")
|
|
201
|
+
if not (title or url):
|
|
202
|
+
continue
|
|
203
|
+
excerpts = item.get("excerpts") or []
|
|
204
|
+
snippet = str(excerpts[0])[:300] if excerpts else ""
|
|
205
|
+
results.append({
|
|
206
|
+
"title": title,
|
|
207
|
+
"url": url,
|
|
208
|
+
"snippet": snippet,
|
|
209
|
+
"source": "parallel_free",
|
|
210
|
+
"published_at": str(item.get("publish_date") or "")[:64],
|
|
211
|
+
})
|
|
212
|
+
return results
|
|
213
|
+
return _engine
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ── Seltz 搜索(api.seltz.ai,2026 新兴 agent 搜索)───────────────────────────
|
|
217
|
+
|
|
218
|
+
_SELTZ_URL = "https://api.seltz.ai/v1/search"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _seltz_key() -> str:
|
|
222
|
+
# get_env 候选链(2026-09-29,issue #12 同类,同 _parallel_key)
|
|
223
|
+
return get_env(["ARGO_SELTZ_API_KEY", "SELTZ_API_KEY"])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
_SELTZ_SCOPES = ("news", "wikipedia", "people", "companies")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _build_seltz_engine(spec: dict[str, Any]) -> Any:
|
|
230
|
+
"""Seltz Search:POST {query, max_results, scope},响应 {documents:[...]}。
|
|
231
|
+
|
|
232
|
+
官方文档 docs.seltz.ai:x-api-key 头鉴权。响应无 title 字段(content
|
|
233
|
+
即摘录正文,首行截作标题);无 score/confidence 字段,排序即上游相关度。
|
|
234
|
+
注册赠 20000 次搜索额度(一次性),故配额按 month/20000 保守看管。
|
|
235
|
+
|
|
236
|
+
**scope 是语料选择,不是可选项**:上游只支持 news / wikipedia / people /
|
|
237
|
+
companies 四个语料,不传一律落 news。2026-09-16 实测:不传 scope 时技术
|
|
238
|
+
查询(「RRF 融合算法」)拿到的是 news 语料里的时政与播客条目,看上去像
|
|
239
|
+
「引用跑偏」,实为语料选错而非上游坏了。四个语料实测:companies 查公司
|
|
240
|
+
概况最好;news 在真新闻查询上可用但覆盖窄;wikipedia 干净;people 不可用
|
|
241
|
+
(10 条全 linkedin.com,人名查不到)。故本引擎按 spec.scope 声明语料,
|
|
242
|
+
config 里把 coverage 收到对应域,不冒充通用源。
|
|
243
|
+
"""
|
|
244
|
+
timeout = spec.get("timeout", 15)
|
|
245
|
+
scope = str(spec.get("scope") or "").strip().lower()
|
|
246
|
+
if scope and scope not in _SELTZ_SCOPES:
|
|
247
|
+
logger.warning(f"seltz: 未知 scope {scope!r}(上游会返回 404),本轮不传 scope")
|
|
248
|
+
scope = ""
|
|
249
|
+
|
|
250
|
+
@safe_search
|
|
251
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None,
|
|
252
|
+
**kwargs) -> list[dict[str, Any]]:
|
|
253
|
+
key = _seltz_key()
|
|
254
|
+
if not key:
|
|
255
|
+
return [{"error": "SELTZ_API_KEY 未设置", "source": "seltz"}]
|
|
256
|
+
to = _timeout or timeout
|
|
257
|
+
limit = max(1, int(n or 5))
|
|
258
|
+
body: dict[str, Any] = {"query": query, "max_results": min(limit, 10)}
|
|
259
|
+
if scope:
|
|
260
|
+
body["scope"] = scope
|
|
261
|
+
req = urllib.request.Request(
|
|
262
|
+
_SELTZ_URL,
|
|
263
|
+
data=json.dumps(body).encode("utf-8"),
|
|
264
|
+
headers={"x-api-key": key, "Content-Type": "application/json"},
|
|
265
|
+
)
|
|
266
|
+
try:
|
|
267
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
268
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.warning(f"seltz 请求失败: {e}")
|
|
271
|
+
return []
|
|
272
|
+
results: list[dict[str, Any]] = []
|
|
273
|
+
for item in (data.get("documents") or [])[:limit]:
|
|
274
|
+
url = str(item.get("url") or "")
|
|
275
|
+
content = str(item.get("content") or "")
|
|
276
|
+
# 官方响应无 title 字段;content 是 markdown,首行截标题时剥掉
|
|
277
|
+
# 井号标题标记(实测冒烟首行形如 "# Building a Rust HTTP/1.1 server...")
|
|
278
|
+
title = str(item.get("title") or "") or content.split("\n")[0].lstrip("# ").strip()[:120]
|
|
279
|
+
if not (title or url):
|
|
280
|
+
continue
|
|
281
|
+
results.append({
|
|
282
|
+
"title": title[:200],
|
|
283
|
+
"url": url,
|
|
284
|
+
"snippet": content[:300],
|
|
285
|
+
"source": "seltz",
|
|
286
|
+
"published_at": str(item.get("published_date") or "")[:64],
|
|
287
|
+
})
|
|
288
|
+
return results
|
|
289
|
+
return _engine
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ── You.com 搜索(ydc-index.io)───────────────────────────────────────────────
|
|
293
|
+
_YOU_URL = "https://ydc-index.io/v1/search"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _you_key() -> str:
|
|
297
|
+
# get_env 候选链(2026-09-29,issue #12 同类,同 _parallel_key)
|
|
298
|
+
return get_env(["ARGO_YDC_API_KEY", "YDC_API_KEY"])
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _you_freshness(query: str) -> str:
|
|
302
|
+
"""freshness 动态化:时效敏感查询(复用缓存层检测)→ day;否则省略(全量)。"""
|
|
303
|
+
try:
|
|
304
|
+
from cache import is_freshness_sensitive_query
|
|
305
|
+
if is_freshness_sensitive_query(query or ""):
|
|
306
|
+
return "day"
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
return ""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _you_language(query: str) -> str:
|
|
313
|
+
"""language 下推:含中文 → zh,其余 en。"""
|
|
314
|
+
try:
|
|
315
|
+
from lang_detect import detect_language
|
|
316
|
+
lang = detect_language(query or "")
|
|
317
|
+
if lang:
|
|
318
|
+
return {"zh": "zh", "ja": "ja"}.get(lang, "en")
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
return "zh" if any("\u4e00" <= c <= "\u9fff" for c in (query or "")) else "en"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _build_you_engine(spec: dict[str, Any]) -> Any:
|
|
325
|
+
"""You.com Web Search:web+news 合并,freshness/language 动态化,page_age → published_at。"""
|
|
326
|
+
timeout = spec.get("timeout", 12)
|
|
327
|
+
|
|
328
|
+
@safe_search
|
|
329
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
330
|
+
to = _timeout or timeout
|
|
331
|
+
key = _you_key()
|
|
332
|
+
if not key:
|
|
333
|
+
return [{"error": "YDC_API_KEY 未设置", "source": "you"}]
|
|
334
|
+
body: dict[str, Any] = {"query": query, "count": max(1, int(n or 5))}
|
|
335
|
+
freshness = _you_freshness(query)
|
|
336
|
+
if freshness:
|
|
337
|
+
body["freshness"] = freshness
|
|
338
|
+
body["language"] = _you_language(query)
|
|
339
|
+
req = urllib.request.Request(
|
|
340
|
+
_YOU_URL,
|
|
341
|
+
data=json.dumps(body).encode("utf-8"),
|
|
342
|
+
headers={"X-API-Key": key, "Content-Type": "application/json"},
|
|
343
|
+
)
|
|
344
|
+
try:
|
|
345
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
346
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.warning(f"you 请求失败: {e}")
|
|
349
|
+
return []
|
|
350
|
+
results: list[dict[str, Any]] = []
|
|
351
|
+
for kind in ("news", "web"):
|
|
352
|
+
for item in (data.get("results", {}).get(kind) or [])[: max(1, int(n or 5))]:
|
|
353
|
+
title = str(item.get("title") or "")[:200]
|
|
354
|
+
url = str(item.get("url") or "")
|
|
355
|
+
if not (title or url):
|
|
356
|
+
continue
|
|
357
|
+
results.append({
|
|
358
|
+
"title": title,
|
|
359
|
+
"url": url,
|
|
360
|
+
"snippet": str(item.get("description") or "")[:300],
|
|
361
|
+
"source": f"you_{kind}",
|
|
362
|
+
# page_age 是 ISO 8601 时间戳(如 2026-08-11T16:24:41)→ 取日期
|
|
363
|
+
"published_at": str(item.get("page_age") or "")[:10],
|
|
364
|
+
})
|
|
365
|
+
return results
|
|
366
|
+
return _engine
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ── Qwant(欧盟 GDPR 合规)──────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
def _build_qwant_engine(spec: dict[str, Any]) -> Any:
|
|
372
|
+
"""Qwant 搜索(qwant.com/?q=,HTML 解析,欧盟 GDPR 合规)"""
|
|
373
|
+
timeout = spec.get("timeout", 10)
|
|
374
|
+
|
|
375
|
+
@safe_search
|
|
376
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
377
|
+
import urllib.parse as up
|
|
378
|
+
to = _timeout or timeout
|
|
379
|
+
url = "https://www.qwant.com/?q=" + up.quote(query)
|
|
380
|
+
headers = {"User-Agent": "Mozilla/5.0", "Referer": "https://www.qwant.com/"}
|
|
381
|
+
req = urllib.request.Request(url, headers=headers)
|
|
382
|
+
try:
|
|
383
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
384
|
+
page = resp.read().decode("utf-8", "replace")
|
|
385
|
+
results = []
|
|
386
|
+
for m in re.finditer(r'<a[^>]*href="(https?://[^"]+)"[^>]*>(.*?)</a>', page, re.S):
|
|
387
|
+
url_m, title_m = m.group(1), re.sub(r'<[^>]+>', '', m.group(2)).strip()
|
|
388
|
+
if not title_m or not url_m or "qwant" in url_m or len(title_m) < 5:
|
|
389
|
+
continue
|
|
390
|
+
results.append({
|
|
391
|
+
"title": title_m[:80],
|
|
392
|
+
"url": url_m,
|
|
393
|
+
"snippet": "",
|
|
394
|
+
"source": "qwant",
|
|
395
|
+
"score": max(1.0 - len(results) * 0.1, 0.1),
|
|
396
|
+
})
|
|
397
|
+
if len(results) >= n:
|
|
398
|
+
break
|
|
399
|
+
return results
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.warning(f"Qwant 搜索失败: {e}")
|
|
402
|
+
return []
|
|
403
|
+
return _engine
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ── Ecosia(环保隐私引擎)───────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
def _build_ecosia_engine(spec: dict[str, Any]) -> Any:
|
|
409
|
+
"""Ecosia 搜索(ecosia.org/search?q=,HTML 解析,环保隐私引擎)"""
|
|
410
|
+
timeout = spec.get("timeout", 10)
|
|
411
|
+
|
|
412
|
+
@safe_search
|
|
413
|
+
def _engine(query: str, n: int = 5, _timeout: float | None = None, **kwargs) -> list[dict[str, Any]]:
|
|
414
|
+
import urllib.parse as up
|
|
415
|
+
to = _timeout or timeout
|
|
416
|
+
url = "https://www.ecosia.org/search?q=" + up.quote(query)
|
|
417
|
+
headers = {"User-Agent": "Mozilla/5.0", "Referer": "https://www.ecosia.org/"}
|
|
418
|
+
req = urllib.request.Request(url, headers=headers)
|
|
419
|
+
try:
|
|
420
|
+
with http_open(req, timeout=to, engine=spec.get("_name", "")) as resp:
|
|
421
|
+
page = resp.read().decode("utf-8", "replace")
|
|
422
|
+
results = []
|
|
423
|
+
for m in re.finditer(r'<a[^>]*href="(https?://[^"]+)"[^>]*>(.*?)</a>', page, re.S):
|
|
424
|
+
url_m, title_m = m.group(1), re.sub(r'<[^>]+>', '', m.group(2)).strip()
|
|
425
|
+
if not title_m or not url_m or "ecosia" in url_m or len(title_m) < 5:
|
|
426
|
+
continue
|
|
427
|
+
results.append({
|
|
428
|
+
"title": title_m[:80],
|
|
429
|
+
"url": url_m,
|
|
430
|
+
"snippet": "",
|
|
431
|
+
"source": "ecosia",
|
|
432
|
+
"score": max(1.0 - len(results) * 0.1, 0.1),
|
|
433
|
+
})
|
|
434
|
+
if len(results) >= n:
|
|
435
|
+
break
|
|
436
|
+
return results
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logger.warning(f"Ecosia 搜索失败: {e}")
|
|
439
|
+
return []
|
|
440
|
+
return _engine
|