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.
Files changed (472) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +735 -0
  3. package/README.es.md +777 -0
  4. package/README.ja.md +777 -0
  5. package/README.ko.md +777 -0
  6. package/README.md +778 -320
  7. package/SKILL.md +135 -0
  8. package/assets/readme/hero.svg +121 -0
  9. package/assets/readme/made-with-beautify.svg +10 -0
  10. package/assets/readme/proof-routes.svg +73 -0
  11. package/assets/readme/why-better.svg +104 -0
  12. package/assets/readme/workflow.svg +83 -0
  13. package/backends/domain_profiles.json +2356 -96
  14. package/backends/engine_registry.yaml +3428 -503
  15. package/backends/query_synonyms_cn.json +35 -0
  16. package/backends/quota_profiles.json +2488 -160
  17. package/backends/source_types_cn.json +109 -0
  18. package/backends/text_lexicon.json +44 -0
  19. package/bin/argo +772 -0
  20. package/bin/argo.js +102 -39
  21. package/config.yaml +5139 -367
  22. package/cordis.patch.yml +21 -0
  23. package/data/countries.json +40 -0
  24. package/data/lang_matrix/lang_capability.json +842 -0
  25. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  26. package/data/regions_cn.json +1 -0
  27. package/docs/ADDING_NEW_ENGINE.md +184 -0
  28. package/docs/ARGO_INTRO.md +124 -0
  29. package/docs/DATA_SOURCE_GAPS.md +81 -0
  30. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  31. package/docs/ENGINE_CATALOG.md +575 -0
  32. package/docs/MCP_SETUP.md +108 -0
  33. package/docs/PLATFORM_PATHS.md +111 -0
  34. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  35. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  36. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  37. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  38. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  39. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  40. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  41. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  42. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  43. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  44. package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
  45. package/docs/SEARCH_ARCHIVE.md +115 -0
  46. package/docs/assets/architecture.svg +104 -0
  47. package/docs/assets/hero.svg +98 -0
  48. package/docs/engines/bangumi.md +27 -0
  49. package/docs/engines/biorxiv.md +27 -0
  50. package/docs/engines/clawhub.md +27 -0
  51. package/docs/engines/crt_sh.md +27 -0
  52. package/docs/engines/datacite.md +27 -0
  53. package/docs/engines/deps_dev.md +27 -0
  54. package/docs/engines/douban_movie.md +27 -0
  55. package/docs/engines/endoflife.md +27 -0
  56. package/docs/engines/flk_law.md +27 -0
  57. package/docs/engines/fxtwitter.md +27 -0
  58. package/docs/engines/google_news.md +27 -0
  59. package/docs/engines/gov_regulations.md +27 -0
  60. package/docs/engines/guqian.md +27 -0
  61. package/docs/engines/hackernews.md +27 -0
  62. package/docs/engines/iplant.md +27 -0
  63. package/docs/engines/keenable.md +27 -0
  64. package/docs/engines/met_museum.md +27 -0
  65. package/docs/engines/nasa_images.md +27 -0
  66. package/docs/engines/netease_music.md +27 -0
  67. package/docs/engines/npm.md +27 -0
  68. package/docs/engines/nvd.md +27 -0
  69. package/docs/engines/openreview.md +27 -0
  70. package/docs/engines/openstd.md +27 -0
  71. package/docs/engines/parallel_free.md +27 -0
  72. package/docs/engines/people_daily.md +27 -0
  73. package/docs/engines/realtime_index.md +27 -0
  74. package/docs/engines/sec_edgar.md +27 -0
  75. package/docs/engines/seltz.md +27 -0
  76. package/docs/engines/skillsmp.md +27 -0
  77. package/docs/engines/sspai.md +27 -0
  78. package/docs/engines/std_samr.md +27 -0
  79. package/docs/engines/train.md +27 -0
  80. package/docs/engines/un_comtrade.md +27 -0
  81. package/docs/engines/wikisource.md +27 -0
  82. package/docs/engines/zdic.md +27 -0
  83. package/docs/engines/zenodo.md +27 -0
  84. package/docs/engines/zhihu_hot_app.md +27 -0
  85. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  86. package/engines/README.md +10 -0
  87. package/engines/_template_cli.yaml +39 -0
  88. package/engines/_template_http.yaml +37 -0
  89. package/engines/plugins/_template_plugin.py +50 -0
  90. package/engines/specs/_cisa_kev.yaml +54 -0
  91. package/engines/specs/_osv.yaml +63 -0
  92. package/engines/specs/aljazeera.yaml +43 -0
  93. package/engines/specs/antara.yaml +43 -0
  94. package/engines/specs/artic.yaml +20 -0
  95. package/engines/specs/aviation_weather.yaml +39 -0
  96. package/engines/specs/bangumi.yaml +21 -0
  97. package/engines/specs/biorxiv.yaml +24 -0
  98. package/engines/specs/carbon_intensity.yaml +32 -0
  99. package/engines/specs/cfets.yaml +41 -0
  100. package/engines/specs/cinii.yaml +44 -0
  101. package/engines/specs/clawhub.yaml +49 -0
  102. package/engines/specs/cleveland.yaml +19 -0
  103. package/engines/specs/cn_ai_news.yaml +43 -0
  104. package/engines/specs/core.yaml +35 -0
  105. package/engines/specs/crt_sh.yaml +38 -0
  106. package/engines/specs/ct24.yaml +43 -0
  107. package/engines/specs/datacite.yaml +27 -0
  108. package/engines/specs/datagov_uk.yaml +41 -0
  109. package/engines/specs/ddgs_images.yaml +45 -0
  110. package/engines/specs/deepseek_search.yaml +74 -0
  111. package/engines/specs/deezer.yaml +20 -0
  112. package/engines/specs/defillama.yaml +38 -0
  113. package/engines/specs/deps_dev.yaml +21 -0
  114. package/engines/specs/digup.yaml +36 -0
  115. package/engines/specs/doi.yaml +39 -0
  116. package/engines/specs/douban_movie.yaml +22 -0
  117. package/engines/specs/dw_news.yaml +47 -0
  118. package/engines/specs/egov_law.yaml +21 -0
  119. package/engines/specs/elpais.yaml +43 -0
  120. package/engines/specs/endoflife.yaml +21 -0
  121. package/engines/specs/energy_charts.yaml +21 -0
  122. package/engines/specs/factcheck_org.yaml +52 -0
  123. package/engines/specs/faz.yaml +43 -0
  124. package/engines/specs/federal_register.yaml +57 -0
  125. package/engines/specs/firecrawl.yaml +53 -0
  126. package/engines/specs/flk_law.yaml +24 -0
  127. package/engines/specs/folha.yaml +43 -0
  128. package/engines/specs/france24.yaml +44 -0
  129. package/engines/specs/full_fact.yaml +47 -0
  130. package/engines/specs/fxtwitter.yaml +36 -0
  131. package/engines/specs/gbfs_nyc.yaml +32 -0
  132. package/engines/specs/gdacs.yaml +21 -0
  133. package/engines/specs/gdelt.yaml +64 -0
  134. package/engines/specs/gleif.yaml +55 -0
  135. package/engines/specs/google_news.yaml +22 -0
  136. package/engines/specs/gov_regulations.yaml +38 -0
  137. package/engines/specs/guardian_rss.yaml +59 -0
  138. package/engines/specs/hkex.yaml +40 -0
  139. package/engines/specs/hurriyet.yaml +43 -0
  140. package/engines/specs/iplant.yaml +25 -0
  141. package/engines/specs/jikan.yaml +24 -0
  142. package/engines/specs/jolpica.yaml +20 -0
  143. package/engines/specs/k10plus.yaml +20 -0
  144. package/engines/specs/keenable.yaml +44 -0
  145. package/engines/specs/keenable_free.yaml +36 -0
  146. package/engines/specs/lefigaro.yaml +43 -0
  147. package/engines/specs/listenbrainz.yaml +19 -0
  148. package/engines/specs/matichon.yaml +43 -0
  149. package/engines/specs/met_museum.yaml +21 -0
  150. package/engines/specs/nasa_images.yaml +45 -0
  151. package/engines/specs/netease_music.yaml +38 -0
  152. package/engines/specs/nhk.yaml +43 -0
  153. package/engines/specs/nhtsa_vpic.yaml +20 -0
  154. package/engines/specs/noaa_swpc.yaml +20 -0
  155. package/engines/specs/nvd.yaml +38 -0
  156. package/engines/specs/obis.yaml +20 -0
  157. package/engines/specs/opencitations.yaml +52 -0
  158. package/engines/specs/openf1.yaml +21 -0
  159. package/engines/specs/openfoodfacts.yaml +45 -0
  160. package/engines/specs/openligadb.yaml +20 -0
  161. package/engines/specs/openreview.yaml +37 -0
  162. package/engines/specs/openstd.yaml +21 -0
  163. package/engines/specs/people_daily.yaml +22 -0
  164. package/engines/specs/realtime_index.yaml +34 -0
  165. package/engines/specs/ror.yaml +20 -0
  166. package/engines/specs/satnogs.yaml +20 -0
  167. package/engines/specs/sec_edgar.yaml +46 -0
  168. package/engines/specs/shfe.yaml +39 -0
  169. package/engines/specs/skillsmp.yaml +50 -0
  170. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  171. package/engines/specs/soilgrids.yaml +25 -0
  172. package/engines/specs/sspai.yaml +36 -0
  173. package/engines/specs/stackexchange.yaml +46 -0
  174. package/engines/specs/std_samr.yaml +21 -0
  175. package/engines/specs/tass.yaml +43 -0
  176. package/engines/specs/tavily_free.yaml +56 -0
  177. package/engines/specs/tle_mirror.yaml +21 -0
  178. package/engines/specs/train.yaml +37 -0
  179. package/engines/specs/tvmaze.yaml +20 -0
  180. package/engines/specs/twitter_syndication.yaml +31 -0
  181. package/engines/specs/un_comtrade.yaml +23 -0
  182. package/engines/specs/unpaywall.yaml +54 -0
  183. package/engines/specs/vnpress.yaml +43 -0
  184. package/engines/specs/weather.yaml +32 -0
  185. package/engines/specs/who_don.yaml +21 -0
  186. package/engines/specs/who_gho.yaml +20 -0
  187. package/engines/specs/wikimedia_commons.yaml +34 -0
  188. package/engines/specs/wikisource.yaml +22 -0
  189. package/engines/specs/worms.yaml +19 -0
  190. package/engines/specs/x_trending.yaml +34 -0
  191. package/engines/specs/yna.yaml +43 -0
  192. package/engines/specs/ynet.yaml +43 -0
  193. package/engines/specs/yuanbao.yaml +93 -0
  194. package/engines/specs/zbmath.yaml +40 -0
  195. package/engines/specs/zdic.yaml +22 -0
  196. package/engines/specs/zenodo.yaml +27 -0
  197. package/engines/specs/zhihu_hot_app.yaml +35 -0
  198. package/mcp/clients.yaml +106 -0
  199. package/package.json +55 -12
  200. package/packages/dsh-plugin/README.md +152 -0
  201. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  202. package/packages/dsh-plugin/dsh/client.js +1533 -0
  203. package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
  204. package/packages/dsh-plugin/dsh/index.js +527 -0
  205. package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
  206. package/packages/dsh-plugin/dsh/runner.js +666 -0
  207. package/packages/dsh-plugin/dsh/wide-research.js +878 -0
  208. package/packages/dsh-plugin/package.json +38 -0
  209. package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
  210. package/packages/dsh-plugin/test/client.test.js +406 -0
  211. package/packages/dsh-plugin/test/config.test.js +30 -0
  212. package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
  213. package/packages/dsh-plugin/test/index.test.js +1382 -0
  214. package/references/query-templates.md +96 -0
  215. package/references/query-templates.yaml +118 -0
  216. package/scripts/ab_eval.py +219 -0
  217. package/scripts/ab_eval_p0p1.py +266 -0
  218. package/scripts/adaptive.py +294 -70
  219. package/scripts/answer.py +154 -0
  220. package/scripts/archive_run.py +591 -0
  221. package/scripts/argo_engine_registry.py +172 -0
  222. package/scripts/argo_paths.py +728 -0
  223. package/scripts/article.py +136 -0
  224. package/scripts/article_meta.py +131 -0
  225. package/scripts/arxiv_source.py +201 -0
  226. package/scripts/badcase_ledger.py +252 -0
  227. package/scripts/batch_probe.py +241 -0
  228. package/scripts/benchmark.py +19 -0
  229. package/scripts/bounded_run.py +144 -0
  230. package/scripts/browser_auth.py +323 -0
  231. package/scripts/budget.py +247 -0
  232. package/scripts/budget_cli.py +93 -0
  233. package/scripts/build_release_assets.sh +65 -0
  234. package/scripts/burstiness_detector.py +147 -0
  235. package/scripts/cache.py +1066 -67
  236. package/scripts/cache_guard.py +271 -0
  237. package/scripts/cache_key_vdom.py +56 -0
  238. package/scripts/cache_similarity.py +170 -0
  239. package/scripts/caliber_isolation.py +195 -0
  240. package/scripts/calibrate_lowquality.py +210 -0
  241. package/scripts/candidate_envelope.py +366 -0
  242. package/scripts/chrome_cdp.py +871 -0
  243. package/scripts/ci_drift_gate.py +90 -0
  244. package/scripts/circuit_breaker.py +389 -0
  245. package/scripts/citations.py +334 -0
  246. package/scripts/cite_cli.py +59 -0
  247. package/scripts/cjk_tokens.py +95 -0
  248. package/scripts/clarify.py +47 -24
  249. package/scripts/cli_io.py +240 -0
  250. package/scripts/config.py +683 -45
  251. package/scripts/config_cache.py +402 -0
  252. package/scripts/conn_pool.py +220 -0
  253. package/scripts/content_boundary.py +46 -0
  254. package/scripts/content_security.py +685 -0
  255. package/scripts/content_signals.py +751 -0
  256. package/scripts/crawl.py +74 -35
  257. package/scripts/cross_lingual_trace.py +293 -0
  258. package/scripts/daemon.py +866 -0
  259. package/scripts/digup_bridge.py +214 -0
  260. package/scripts/dispatch_result.py +98 -0
  261. package/scripts/engine_admission.py +366 -0
  262. package/scripts/engine_dispatch.py +982 -0
  263. package/scripts/engine_env.py +539 -0
  264. package/scripts/engine_failure.py +411 -0
  265. package/scripts/engine_families.py +738 -0
  266. package/scripts/engine_policy.py +218 -0
  267. package/scripts/engine_requires.py +190 -0
  268. package/scripts/engine_status.py +322 -0
  269. package/scripts/engine_validate.py +638 -0
  270. package/scripts/engines.py +799 -294
  271. package/scripts/engines_base.py +1521 -0
  272. package/scripts/engines_builders.py +310 -0
  273. package/scripts/engines_builders_batch10.py +204 -0
  274. package/scripts/engines_builders_batch11.py +174 -0
  275. package/scripts/engines_builders_batch12.py +103 -0
  276. package/scripts/engines_builders_batch13.py +340 -0
  277. package/scripts/engines_builders_batch14.py +656 -0
  278. package/scripts/engines_builders_batch15.py +249 -0
  279. package/scripts/engines_builders_batch9.py +1468 -0
  280. package/scripts/engines_builders_cn.py +2283 -0
  281. package/scripts/engines_builders_data.py +2923 -0
  282. package/scripts/engines_builders_data_macro.py +775 -0
  283. package/scripts/engines_builders_feeds.py +255 -0
  284. package/scripts/engines_builders_intl.py +1239 -0
  285. package/scripts/engines_builders_keyless.py +111 -0
  286. package/scripts/engines_builders_search.py +440 -0
  287. package/scripts/engines_builders_tech.py +1359 -0
  288. package/scripts/evidence.py +620 -166
  289. package/scripts/evidence_loop.py +624 -0
  290. package/scripts/evidence_tier.py +298 -0
  291. package/scripts/except_sets.py +25 -0
  292. package/scripts/extract.py +55 -16
  293. package/scripts/fact_align.py +207 -0
  294. package/scripts/fact_crosscheck.py +230 -0
  295. package/scripts/fetch.py +25 -12
  296. package/scripts/fetch_browser.py +87 -0
  297. package/scripts/fetch_jsrun.py +122 -0
  298. package/scripts/fetch_quality.py +225 -0
  299. package/scripts/fetch_render_tinyfish.py +117 -0
  300. package/scripts/fetch_v3.py +1804 -0
  301. package/scripts/focus_extract.py +350 -0
  302. package/scripts/fulltext_store.py +169 -0
  303. package/scripts/fxtwitter_api.py +325 -0
  304. package/scripts/gen_engine_catalog.py +364 -0
  305. package/scripts/gen_native_tools.py +91 -0
  306. package/scripts/gen_routing_snapshot.py +195 -0
  307. package/scripts/health_check.py +140 -402
  308. package/scripts/health_probe.py +23 -8
  309. package/scripts/hot_state.py +134 -0
  310. package/scripts/http_client.py +1053 -0
  311. package/scripts/image_ops.py +430 -0
  312. package/scripts/image_vision/vision_probe.swift +126 -0
  313. package/scripts/install.ps1 +166 -0
  314. package/scripts/install.sh +221 -0
  315. package/scripts/job.py +1186 -0
  316. package/scripts/jump_resolver.py +235 -0
  317. package/scripts/lang_capability.py +150 -0
  318. package/scripts/lang_detect.py +289 -0
  319. package/scripts/lang_matrix_probe.py +329 -0
  320. package/scripts/lang_pref.py +311 -0
  321. package/scripts/link_source.py +340 -0
  322. package/scripts/local_image.py +970 -0
  323. package/scripts/local_seek.py +402 -0
  324. package/scripts/macro_countries.py +97 -0
  325. package/scripts/matrix_search_eval.py +1187 -0
  326. package/scripts/mcp_compact.py +234 -0
  327. package/scripts/mcp_diag.py +8 -3
  328. package/scripts/mcp_handlers.py +492 -0
  329. package/scripts/mcp_handlers_surface.py +157 -0
  330. package/scripts/mcp_launch.sh +124 -0
  331. package/scripts/mcp_local.py +241 -0
  332. package/scripts/mcp_server.py +125 -419
  333. package/scripts/mcp_setup.py +431 -0
  334. package/scripts/mcp_tools.py +331 -0
  335. package/scripts/mcp_transport.py +191 -0
  336. package/scripts/mcp_warm.py +127 -0
  337. package/scripts/min_version_scan.py +335 -0
  338. package/scripts/net_proxy.py +258 -0
  339. package/scripts/network_aware.py +152 -0
  340. package/scripts/output_transparency.py +200 -0
  341. package/scripts/paid_policy.py +98 -0
  342. package/scripts/paper.py +274 -0
  343. package/scripts/pdf_extract.py +518 -0
  344. package/scripts/phrase_filter.py +66 -0
  345. package/scripts/plan.py +422 -0
  346. package/scripts/postprocess_signals.py +150 -0
  347. package/scripts/pre-commit +48 -0
  348. package/scripts/query_enhance.py +157 -0
  349. package/scripts/query_operators.py +196 -0
  350. package/scripts/query_rewriter.py +116 -7
  351. package/scripts/query_signals.py +300 -0
  352. package/scripts/query_templates.py +167 -0
  353. package/scripts/query_understanding.py +411 -0
  354. package/scripts/query_variants.py +298 -0
  355. package/scripts/quota.py +633 -76
  356. package/scripts/rank_signals.py +775 -0
  357. package/scripts/ranking_eval.py +319 -0
  358. package/scripts/readability_extract.py +433 -0
  359. package/scripts/recompute.py +415 -0
  360. package/scripts/recovery.py +616 -0
  361. package/scripts/recovery_l0.py +160 -0
  362. package/scripts/recovery_runner.py +150 -0
  363. package/scripts/redskill/redskill_engine.py +332 -0
  364. package/scripts/regression_p0p1.py +318 -0
  365. package/scripts/replay_eval.py +658 -0
  366. package/scripts/rerank_tfidf.py +225 -0
  367. package/scripts/research.py +607 -403
  368. package/scripts/research_cli.py +361 -0
  369. package/scripts/research_dossier.py +492 -0
  370. package/scripts/research_expand.py +169 -0
  371. package/scripts/research_gates.py +272 -0
  372. package/scripts/research_report.py +209 -0
  373. package/scripts/research_strategy.py +37 -0
  374. package/scripts/research_work_packages.py +195 -0
  375. package/scripts/result_lang.py +311 -0
  376. package/scripts/retry_policy.py +76 -0
  377. package/scripts/robots_guard.py +259 -0
  378. package/scripts/route.py +921 -254
  379. package/scripts/route_bangs.py +40 -0
  380. package/scripts/route_cache.py +243 -0
  381. package/scripts/route_combo.py +917 -0
  382. package/scripts/route_domains.py +341 -0
  383. package/scripts/route_lang.py +605 -0
  384. package/scripts/route_log.py +63 -0
  385. package/scripts/route_policy.py +250 -0
  386. package/scripts/rrf_classic.py +66 -0
  387. package/scripts/screenshot.py +73 -0
  388. package/scripts/search.py +886 -406
  389. package/scripts/search_attribution.py +185 -0
  390. package/scripts/search_benchmark.py +407 -0
  391. package/scripts/search_cli.py +640 -0
  392. package/scripts/search_entry.py +395 -0
  393. package/scripts/search_output.py +690 -0
  394. package/scripts/search_pipeline.py +926 -0
  395. package/scripts/search_rank.py +991 -0
  396. package/scripts/search_types.py +129 -16
  397. package/scripts/seek_locator.py +68 -0
  398. package/scripts/semantic_evidence.py +391 -0
  399. package/scripts/serp_guard.py +336 -0
  400. package/scripts/serp_kill_log.py +74 -0
  401. package/scripts/single_flight.py +120 -0
  402. package/scripts/social_engines/__init__.py +10 -0
  403. package/scripts/social_engines/bilibili_engine.py +131 -0
  404. package/scripts/social_engines/hackernews_engine.py +113 -0
  405. package/scripts/social_engines/reddit_engine.py +172 -0
  406. package/scripts/social_engines/twitter_engine.py +397 -0
  407. package/scripts/social_engines/v2ex_engine.py +101 -0
  408. package/scripts/social_engines/weibo_engine.py +128 -0
  409. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  410. package/scripts/social_engines/zhihu_engine.py +139 -0
  411. package/scripts/social_research.py +201 -0
  412. package/scripts/sogou_weixin_browser.py +318 -0
  413. package/scripts/source_ledger.py +249 -0
  414. package/scripts/stage_timing.py +58 -0
  415. package/scripts/stats_cli.py +107 -0
  416. package/scripts/straggler_policy.py +60 -0
  417. package/scripts/stylometry_detector.py +231 -0
  418. package/scripts/sync_backends.py +353 -0
  419. package/scripts/sync_installs.py +29 -0
  420. package/scripts/template_cli.py +127 -0
  421. package/scripts/text_lexicon.py +72 -0
  422. package/scripts/tfidf_router.py +109 -37
  423. package/scripts/time_utils.py +233 -0
  424. package/scripts/topic_research_profiles.py +643 -0
  425. package/scripts/train.py +737 -0
  426. package/scripts/tweet.py +596 -0
  427. package/scripts/url_canon.py +217 -0
  428. package/scripts/url_optimize.py +56 -0
  429. package/scripts/url_safety.py +236 -0
  430. package/scripts/usage_log.py +116 -0
  431. package/scripts/usage_log_clean.py +113 -0
  432. package/scripts/v2ex_nodes.py +311 -0
  433. package/scripts/variant_recall.py +188 -0
  434. package/scripts/watch.py +205 -0
  435. package/scripts/wechat_article.py +258 -0
  436. package/scripts/wide_research.py +447 -0
  437. package/scripts/wx.py +349 -0
  438. package/scripts/x_article_fetch.py +141 -0
  439. package/scripts/x_search.py +234 -0
  440. package/scripts/x_trending.py +189 -0
  441. package/scripts/yaml_load.py +67 -0
  442. package/scripts/zhihu_quota.py +139 -0
  443. package/sub-skills/ego-search/SKILL.md +253 -0
  444. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  445. package/sub-skills/ego-search/scripts/merge.py +226 -0
  446. package/sub-skills/ego-search/scripts/quality.py +64 -0
  447. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  448. package/sub-skills/ego-search/scripts/safety.py +109 -0
  449. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  450. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  451. package/sub-skills/js-run/SKILL.md +67 -0
  452. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  453. package/sub-skills/js-run/scripts/shim.js +188 -0
  454. package/sub-skills/local-search/SKILL.md +50 -53
  455. package/sub-skills/local-search/bm25.py +186 -0
  456. package/sub-skills/local-search/cli_runner.py +331 -0
  457. package/sub-skills/local-search/config.yaml +152 -55
  458. package/sub-skills/local-search/engine_registry.py +91 -26
  459. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  460. package/sub-skills/local-search/local_search_adapter.py +13 -2
  461. package/sub-skills/local-search/parse_maps.yaml +40 -42
  462. package/sub-skills/local-search/parsers.py +328 -0
  463. package/sub-skills/local-search/search_v3.py +716 -242
  464. package/sub-skills/local-search/smart_router.py +47 -7
  465. package/sub-skills/local-seek/SKILL.md +110 -0
  466. package/sub-skills/local-seek/config/domains.yaml +20 -0
  467. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  468. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  469. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  470. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  471. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  472. package/sub-skills/local-seek/scripts/seek_rank.py +197 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """engine_registry.py — local-search 引擎注册中心(唯一真源)
2
+ """engine_registry.py — local-search 引擎注册中心(唯一来源)
3
3
 
4
4
  读取 sub-skills/local-search/config.yaml 与 parse_maps.yaml,维护 24+ 本地引擎的
5
5
  元数据、分类与可用状态。新增引擎只需改 YAML,无需改代码。
@@ -11,6 +11,7 @@ import functools
11
11
  import json
12
12
  import logging
13
13
  import os
14
+ import sys
14
15
  import time
15
16
  from pathlib import Path
16
17
  from typing import Any
@@ -29,6 +30,11 @@ SKILL_DIR = Path(__file__).resolve().parent
29
30
  CONFIG_PATH = SKILL_DIR / "config.yaml"
30
31
  PARSE_MAPS_PATH = SKILL_DIR / "parse_maps.yaml"
31
32
 
33
+ # 健康条目陈旧线(与 _load_health/_save_health 共用):30 天未探针的记录对路由
34
+ # 已无参考价值,读入与写回两侧都要过滤——长驻进程的内存快照是全量写回的,
35
+ # 只在读取侧过滤,两天后一次保存就会把 31 天的陈旧条目复活。
36
+ _HEALTH_STALE_S = 30 * 86400
37
+
32
38
  # 领域分类(与 config.yaml 中 engines[*].category 对应)
33
39
  DEFAULT_CATEGORIES = [
34
40
  "web_general",
@@ -40,30 +46,40 @@ DEFAULT_CATEGORIES = [
40
46
  "vertical",
41
47
  ]
42
48
 
43
- # 反爬/拦截标记(用于 health_check 快速判定)
44
- ANTI_BOT_MARKERS = [
45
- "captcha", "recaptcha", "robot", "robots", "cloudflare", "challenge",
46
- "blocked", "verification", "please verify", "access denied",
47
- "too many requests", "rate limit",
48
- ]
49
+ def _ensure_root_scripts() -> None:
50
+ _root_scripts = SKILL_DIR.parents[1] / "scripts"
51
+ if _root_scripts.exists() and str(_root_scripts) not in sys.path:
52
+ sys.path.append(str(_root_scripts))
49
53
 
50
54
 
51
55
  def _load_yaml(path: Path) -> dict[str, Any]:
52
- if yaml is None:
53
- raise ImportError("缺少 PyYAML,请安装:pip install pyyaml")
54
56
  try:
57
+ _ensure_root_scripts()
58
+ from yaml_load import load as _yaml_load
55
59
  with path.open("r", encoding="utf-8") as f:
56
- data = yaml.safe_load(f)
60
+ data = _yaml_load(f)
57
61
  return data if isinstance(data, dict) else {}
58
- except Exception as e:
59
- logger.warning(f"加载 YAML 失败 {path}: {e}")
60
- return {}
62
+ except Exception:
63
+ if yaml is None:
64
+ raise ImportError("缺少 PyYAML,请安装:pip install pyyaml")
65
+ try:
66
+ with path.open("r", encoding="utf-8") as f:
67
+ data = yaml.safe_load(f)
68
+ return data if isinstance(data, dict) else {}
69
+ except Exception as e:
70
+ logger.warning(f"加载 YAML 失败 {path}: {e}")
71
+ return {}
61
72
 
62
73
 
63
74
  def _default_health_state_path() -> Path:
64
- base = Path(os.path.expanduser("~/.cache/unified-search"))
65
- base.mkdir(parents=True, exist_ok=True)
66
- return base / "local_search_health.json"
75
+ try:
76
+ _ensure_root_scripts()
77
+ from argo_paths import state_path
78
+ return state_path("local_search_health.json")
79
+ except Exception:
80
+ base = Path(os.path.expanduser("~/.cache/unified-search"))
81
+ base.mkdir(parents=True, exist_ok=True)
82
+ return base / "local_search_health.json"
67
83
 
68
84
 
69
85
  class EngineRegistry:
@@ -77,7 +93,8 @@ class EngineRegistry:
77
93
  ):
78
94
  self.config_path = Path(config_path) if config_path else CONFIG_PATH
79
95
  self.parse_maps_path = Path(parse_maps_path) if parse_maps_path else PARSE_MAPS_PATH
80
- self.health_state_path = Path(health_state_path) if health_state_path else _default_health_state_path()
96
+ self._explicit_health_path = Path(health_state_path) if health_state_path else None
97
+ self._loaded_health_path: Path | None = None
81
98
  self._config_mtime: float = 0.0
82
99
  self._parse_mtime: float = 0.0
83
100
  self._config: dict[str, Any] = {}
@@ -85,6 +102,19 @@ class EngineRegistry:
85
102
  self._health: dict[str, Any] = {}
86
103
  self._load()
87
104
 
105
+ @property
106
+ def health_state_path(self) -> Path:
107
+ return self._explicit_health_path or _default_health_state_path()
108
+
109
+ @health_state_path.setter
110
+ def health_state_path(self, val: Path | str | None) -> None:
111
+ self._explicit_health_path = Path(val) if val else None
112
+
113
+ def _sync_health_path(self) -> None:
114
+ cur = self.health_state_path
115
+ if self._loaded_health_path != cur:
116
+ self._health = self._load_health()
117
+
88
118
  def _load(self) -> None:
89
119
  """加载配置、解析映射和健康状态缓存。"""
90
120
  cfg = _load_yaml(self.config_path)
@@ -99,22 +129,44 @@ class EngineRegistry:
99
129
  self._health = self._load_health()
100
130
 
101
131
  def _load_health(self) -> dict[str, Any]:
102
- if not self.health_state_path.exists():
132
+ hp = self.health_state_path
133
+ self._loaded_health_path = hp
134
+ if not hp.exists():
103
135
  return {}
104
136
  try:
105
- with self.health_state_path.open("r", encoding="utf-8") as f:
137
+ with hp.open("r", encoding="utf-8") as f:
106
138
  data = json.load(f)
107
139
  if isinstance(data, dict):
108
- return data
140
+ # 陈旧条目清理(2026-09-29):健康文件此前只增不减——实测
141
+ # 有条目 49 天未刷新、还有测试引擎名残留。30 天未探针的记录
142
+ # 对路由已无参考价值(引擎配置都可能换过),读入时丢弃。
143
+ cutoff = time.time() - _HEALTH_STALE_S
144
+ return {k: v for k, v in data.items()
145
+ if isinstance(v, dict) and v.get("last_checked", 0) >= cutoff}
109
146
  except Exception as e:
110
147
  logger.warning(f"加载健康状态失败: {e}")
111
148
  return {}
112
149
 
113
150
  def _save_health(self) -> None:
114
151
  try:
115
- self.health_state_path.parent.mkdir(parents=True, exist_ok=True)
116
- with self.health_state_path.open("w", encoding="utf-8") as f:
117
- json.dump(self._health, f, ensure_ascii=False, indent=2)
152
+ hp = self.health_state_path
153
+ self._loaded_health_path = hp
154
+ # 写回前套用与 _load_health 相同的陈旧过滤:内存快照是全量写回的,
155
+ # 不过滤会让 30 天未刷新的条目复活并覆盖他进程新写的条目。
156
+ cutoff = time.time() - _HEALTH_STALE_S
157
+ payload = {k: v for k, v in self._health.items()
158
+ if isinstance(v, dict) and v.get("last_checked", 0) >= cutoff}
159
+ hp.parent.mkdir(parents=True, exist_ok=True)
160
+ try:
161
+ _ensure_root_scripts()
162
+ from argo_paths import atomic_write_json, file_lock
163
+ with file_lock(hp):
164
+ atomic_write_json(hp, payload)
165
+ except ImportError:
166
+ tmp = hp.with_name(hp.name + f".{os.getpid()}.tmp")
167
+ with tmp.open("w", encoding="utf-8") as f:
168
+ json.dump(payload, f, ensure_ascii=False, indent=2)
169
+ os.replace(tmp, hp)
118
170
  except Exception as e:
119
171
  logger.warning(f"保存健康状态失败: {e}")
120
172
 
@@ -150,6 +202,7 @@ class EngineRegistry:
150
202
 
151
203
  def get_engine(self, name: str) -> dict[str, Any] | None:
152
204
  """获取指定引擎的完整配置(含运行时可用状态)。"""
205
+ self._sync_health_path()
153
206
  spec = self.engines.get(name)
154
207
  if spec is None:
155
208
  return None
@@ -211,26 +264,37 @@ class EngineRegistry:
211
264
  # ── 可用性管理 ───────────────────────────────────────────────────────────────
212
265
 
213
266
  def get_health(self, name: str) -> dict[str, Any]:
267
+ self._sync_health_path()
214
268
  return dict(self._health.get(name, {}))
215
269
 
216
- def update_availability(self, name: str, available: bool, **extra: Any) -> None:
217
- """更新单个引擎的可用状态,并持久化。"""
270
+ def update_availability(self, name: str, available: bool,
271
+ persist: bool = True, **extra: Any) -> None:
272
+ """更新单个引擎的可用状态。
273
+
274
+ persist=False 时只改内存、不落盘——批量更新方(run_health_check 一轮
275
+ N 个引擎)用它把 N 次全文件重写收成 1 次,由调用方最后统一 _save_health。
276
+ """
277
+ self._sync_health_path()
218
278
  now = time.time()
219
279
  record = self._health.setdefault(name, {})
220
280
  record["last_checked"] = now
221
281
  record["available"] = available
222
282
  if available:
223
283
  record["consecutive_failures"] = 0
284
+ # 连续成功计数:健康门自适应 TTL 的依据(稳定引擎拉长探针间隔)
285
+ record["consecutive_ok"] = record.get("consecutive_ok", 0) + 1
224
286
  record["last_ok"] = now
225
287
  record["fail_reason"] = None
226
288
  else:
227
289
  record["consecutive_failures"] = record.get("consecutive_failures", 0) + 1
290
+ record["consecutive_ok"] = 0
228
291
  if extra.get("fail_reason"):
229
292
  record["fail_reason"] = extra["fail_reason"]
230
293
  for k, v in extra.items():
231
294
  if k not in ("last_checked", "available", "consecutive_failures"):
232
295
  record[k] = v
233
- self._save_health()
296
+ if persist:
297
+ self._save_health()
234
298
 
235
299
  def bulk_update_availability(self, updates: dict[str, dict[str, Any]]) -> None:
236
300
  """批量更新可用状态。"""
@@ -238,6 +302,7 @@ class EngineRegistry:
238
302
  self.update_availability(name, data.get("available", False), **data)
239
303
 
240
304
  def is_available(self, name: str) -> bool:
305
+ self._sync_health_path()
241
306
  eng = self.get_engine(name)
242
307
  if eng is not None:
243
308
  return bool(eng.get("available", True))
@@ -17,6 +17,8 @@ import json
17
17
  import logging
18
18
  import os
19
19
  import re
20
+ import shutil
21
+ import sys
20
22
  import time
21
23
  import urllib.error
22
24
  import urllib.parse
@@ -26,6 +28,11 @@ from typing import Any
26
28
 
27
29
  from engine_registry import EngineRegistry, get_registry, update_availability
28
30
 
31
+ # scripts/ 入路径(net_proxy 出口需要;append 不抢占子技能同名模块的优先级)
32
+ _ROOT_SCRIPTS = Path(__file__).resolve().parents[2] / "scripts"
33
+ if _ROOT_SCRIPTS.exists() and str(_ROOT_SCRIPTS) not in sys.path:
34
+ sys.path.append(str(_ROOT_SCRIPTS))
35
+
29
36
  logger = logging.getLogger("local_search.health_check")
30
37
  if not logger.handlers:
31
38
  logger.setLevel(logging.WARNING)
@@ -41,6 +48,21 @@ ANTI_BOT_MARKERS = [
41
48
  # HTTP 状态码分组
42
49
  RETRYABLE_STATUS = {429, 503, 502, 504}
43
50
 
51
+ # 自适应 TTL 上限(秒)
52
+ _MAX_HEALTH_TTL_S = 3600.0
53
+
54
+
55
+ def _effective_ttl(rec: dict[str, Any], base_ttl_s: float) -> float:
56
+ """连续成功 → 探测间隔指数放宽(×2^min(streak,3),硬上限 60min)。
57
+
58
+ 探针不是免费的:HTTP canary 是直连上游的真实请求(最坏 8s 墙钟,且
59
+ 每 5min 对 baidu/sogou/360 这类激进反爬源发一次固定「test」查询,纯增
60
+ 封号风险)。稳定可用的引擎不需要高频确认;任何一次失败 consecutive_ok
61
+ 归零,立即回到基准 TTL。
62
+ """
63
+ streak = int(rec.get("consecutive_ok", 0) or 0)
64
+ return min(base_ttl_s * (2 ** min(max(streak, 0), 3)), _MAX_HEALTH_TTL_S)
65
+
44
66
 
45
67
  def _now() -> float:
46
68
  return time.time()
@@ -96,9 +118,19 @@ def _fetch_probe(
96
118
  req_headers.setdefault("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
97
119
 
98
120
  req = urllib.request.Request(url, headers=req_headers, method=method)
121
+ # 出口统一走 net_proxy.open_url(与 search_v3._fetch 同语义):裸 urlopen
122
+ # 不认 argo 在 config.yaml network.proxy 里配置的 url/rules——「必须经代理
123
+ # 才能出网」的环境里探针全部连不上,所有 HTTP 引擎被判 unavailable,
124
+ # fast/budget 路由随后被静默清空,症状与「源挂了」完全一样(主仓
125
+ # issue #13 同形态,13 个模块已修,这里是漏网的一个)。scripts/ 不在
126
+ # 路径(独立打包形态)时回退裸 urlopen,行为与改动前一致。
127
+ try:
128
+ from net_proxy import open_url
129
+ except ImportError:
130
+ open_url = urllib.request.urlopen # type: ignore[assignment]
99
131
  t0 = time.time()
100
132
  try:
101
- with urllib.request.urlopen(req, timeout=timeout) as resp:
133
+ with open_url(req, timeout=timeout) as resp:
102
134
  raw = resp.read()
103
135
  try:
104
136
  text = raw.decode("utf-8", errors="replace")
@@ -149,6 +181,73 @@ def _parse_success(engine_name: str, text: str, fmt: str, registry: EngineRegist
149
181
  return len(text) > 200
150
182
 
151
183
 
184
+ def _check_cli_engine(engine_name: str, spec: dict[str, Any],
185
+ timeout: float = 8,
186
+ canary_query: str = "test", n: int = 1) -> dict[str, Any]:
187
+ """CLI 引擎健康探针:跑一次真实 canary 查询,校验「能出结果」。
188
+
189
+ 2026-10-06 修复假阳性:此前只跑 `ddgs --help`——二进制存在即判
190
+ available。ddgs 9.14.4 移除 bing text 后端、brave/yahoo 对真实查询
191
+ 稳定 0 结果后,`--help` 全绿而搜索全灭,死引擎反复混入组合(本地
192
+ 兜底链 4.5-8.3s 的结构性成因之一)。探针与搜索同路径(复用
193
+ search_v3._run_cli_engine),canary 拿不到 ≥1 条结果即失败。
194
+ 代价:一次真实查询的墙钟(仅健康记录过期时发生,TTL 5min 起)。
195
+ search_v3 不可导入时退化为 --help 探测(fail-open,不崩探针链)。
196
+ """
197
+ cli_cmd = spec.get("cli_command", "")
198
+ if not cli_cmd:
199
+ return {"name": engine_name, "url": "", "status": None,
200
+ "latency_ms": 0, "parse_ok": False, "text_sample": "",
201
+ "fail_reason": "missing_cli_command", "available": False}
202
+ # which 短路:二进制不在 PATH 上时不再白付一次 --help 子进程 spawn
203
+ # (ddgs 冷启实测 0.35s)。与 search_v3._check_cli_available 同判据;
204
+ # shim 损坏(能 which 到但跑不动)仍由下方真实调用暴露。
205
+ if shutil.which(cli_cmd) is None:
206
+ return {"name": engine_name, "url": "", "status": None,
207
+ "latency_ms": 0, "parse_ok": False, "text_sample": "",
208
+ "fail_reason": "cli_not_found", "available": False}
209
+ import subprocess
210
+ t0 = time.time()
211
+ fail_reason = None
212
+ ok = False
213
+ text_sample = ""
214
+ try:
215
+ try:
216
+ from search_v3 import _run_cli_engine # 同目录,探针与搜索同路径
217
+ except ImportError:
218
+ _run_cli_engine = None # type: ignore[assignment]
219
+ if _run_cli_engine is not None:
220
+ results, err = _run_cli_engine(spec, canary_query, n,
221
+ min(timeout, 8))
222
+ ok = bool(results)
223
+ if not ok:
224
+ fail_reason = (err or "no_results")[:120]
225
+ text_sample = (err or "")[:300]
226
+ else:
227
+ # 退化路径:search_v3 不可用,只验证可执行(fail-open)。
228
+ # 真正的搜索失败由执行层熔断器记录。
229
+ result = subprocess.run(
230
+ [cli_cmd, "--help"],
231
+ capture_output=True, text=True, timeout=min(timeout, 5),
232
+ encoding="utf-8", errors="replace", # Windows GBK locale 下 UTF-8 输出会解码崩
233
+ )
234
+ ok = result.returncode == 0
235
+ if not ok:
236
+ fail_reason = f"cli_exit_{result.returncode}"
237
+ except subprocess.TimeoutExpired:
238
+ ok, fail_reason = False, "cli_timeout"
239
+ except FileNotFoundError:
240
+ ok, fail_reason = False, "cli_not_found"
241
+ except Exception as e: # 探针自身失败不判引擎死(fail-open):记为软失败
242
+ ok, fail_reason = False, f"probe_error:{e}"[:120]
243
+ return {
244
+ "name": engine_name, "url": "", "status": 200 if ok else None,
245
+ "latency_ms": round((time.time() - t0) * 1000, 2),
246
+ "parse_ok": ok, "text_sample": text_sample,
247
+ "fail_reason": fail_reason, "available": ok,
248
+ }
249
+
250
+
152
251
  def check_engine(
153
252
  engine_name: str,
154
253
  registry: EngineRegistry | None = None,
@@ -164,6 +263,11 @@ def check_engine(
164
263
  if not spec.get("enabled", True):
165
264
  return {"name": engine_name, "available": False, "fail_reason": "disabled"}
166
265
 
266
+ # cli 引擎无 HTTP 端点,走 CLI 探针而非 URL 探针
267
+ if spec.get("type") == "cli":
268
+ return _check_cli_engine(engine_name, spec, timeout=timeout,
269
+ canary_query=canary_query, n=n)
270
+
167
271
  url, headers = _build_canary_url(spec, canary_query, n)
168
272
  method = spec.get("method", "GET")
169
273
  status, latency_ms, text, fail_reason = _fetch_probe(url, headers, method=method, timeout=timeout)
@@ -254,13 +358,18 @@ def run_health_check(
254
358
  enabled = engine_names if engine_names is not None else reg.list_engines(enabled_only=True)
255
359
 
256
360
  now = _now()
257
- # TTL 检查:在有效期内直接复用缓存
361
+ # TTL 检查:在有效期内直接复用缓存。
362
+ # 按引擎名逐一比对(此前 cached.values() 里的 dict 与 enabled 的 str 比较
363
+ # 恒 False → all(空) 恒 True → 健康状态永不刷新),任一引擎记录缺失或
364
+ # 过期即重新探针。TTL 按连续成功自适应放宽(_effective_ttl)。
258
365
  cached = reg._health
259
- all_recent = all(
260
- (now - h.get("last_checked", 0)) < ttl_minutes * 60
261
- for h in cached.values() if engine_names is None or h in enabled
262
- )
263
- if cached and all_recent:
366
+
367
+ def _recent(name: str) -> bool:
368
+ rec = cached.get(name)
369
+ return bool(rec) and (now - rec.get("last_checked", 0)) < _effective_ttl(rec, ttl_minutes * 60)
370
+
371
+ all_recent = bool(enabled) and all(_recent(n) for n in enabled)
372
+ if all_recent:
264
373
  return {name: dict(h) for name, h in cached.items() if name in enabled}
265
374
 
266
375
  reports: dict[str, dict[str, Any]] = {}
@@ -278,8 +387,9 @@ def run_health_check(
278
387
  for name, report in reports.items():
279
388
  previous = reg.get_health(name)
280
389
  final_available = apply_threshold(report, previous)
390
+ # available 以位置参数传入,勿再放进 kwargs(此前重复键触发
391
+ # TypeError,被 #⑨ 缓存恒命中掩盖,健康检查从未真正执行)
281
392
  update_data = {
282
- "available": final_available,
283
393
  "latency_ms": report.get("latency_ms"),
284
394
  "status": report.get("status"),
285
395
  "parse_ok": report.get("parse_ok"),
@@ -289,7 +399,11 @@ def run_health_check(
289
399
  update_data["success_rate"] = 1.0
290
400
  else:
291
401
  update_data["success_rate"] = 0.0
292
- reg.update_availability(name, final_available, **update_data)
402
+ # persist=False:一轮 N 个引擎收成 1 次落盘(此前每引擎全文件重写一次)
403
+ reg.update_availability(name, final_available, persist=False, **update_data)
404
+
405
+ if reports:
406
+ reg._save_health()
293
407
 
294
408
  return reports
295
409
 
@@ -310,8 +424,24 @@ def get_available_engines(
310
424
  now = _now()
311
425
  ttl = reg.settings.get("health_check", {}).get("ttl_minutes", 5) * 60
312
426
  cached = reg._health
313
- if cached and all((now - h.get("last_checked", 0)) < ttl for h in cached.values() if h in engine_names):
427
+
428
+ def _recent(name: str) -> bool:
429
+ rec = cached.get(name)
430
+ # TTL 按连续成功自适应放宽:稳定引擎最多放宽到 60min,
431
+ # 探针墙钟与对反爬源的 canary 流量随之下降一个量级。
432
+ return bool(rec) and (now - rec.get("last_checked", 0)) < _effective_ttl(rec, ttl)
433
+
434
+ # 逐引擎名比对新鲜度(此前 dict in list[str] 恒 False → 恒命中,
435
+ # 健康状态永不刷新)。
436
+ # 2026-10-06:只探**过期**的引擎——此前 all() 判全新鲜,任一过期即
437
+ # 全量重探,稳定引擎(TTL 放宽到 60min)被反复拖进探针浪;探针浪
438
+ # 实测 ~2-4.5s(yahoo 慢空),全量重探把它加到每次调用路径上。
439
+ # 注意 engine_names 在上方已保证非空(None → 全量启用列表)。
440
+ stale = [n for n in engine_names if not _recent(n)]
441
+ if not stale:
314
442
  return [n for n in engine_names if reg.is_available(n)]
443
+ run_health_check(registry=reg, engine_names=stale)
444
+ return [n for n in engine_names if reg.is_available(n)]
315
445
  run_health_check(registry=reg, engine_names=engine_names)
316
446
  return [n for n in engine_names if reg.is_available(n)]
317
447
 
@@ -2,7 +2,7 @@
2
2
  """local_search_adapter.py — local-search 兼容入口
3
3
 
4
4
  保留原 adapter 接口,内部委托 search_v3.search_engines 执行。
5
- unified-search 可通过 --sub-skill local-search 或 --local-first 调用本入口。
5
+ unified-search 可通过 --local-first 调用本入口。
6
6
  """
7
7
 
8
8
  from __future__ import annotations
@@ -29,10 +29,18 @@ def main():
29
29
  parser.add_argument("--engine", "-e", default="", help="引擎名,多个用逗号分隔")
30
30
  parser.add_argument("--n", type=int, default=5, help="每引擎结果数")
31
31
  parser.add_argument("--timeout", "-t", type=float, default=None, help="超时秒数")
32
- parser.add_argument("--max-parallel", type=int, default=5)
32
+ # None = 以 config 的 max_parallel_engines 为真源(与主链进程内路径同口径)
33
+ parser.add_argument("--max-parallel", type=int, default=None)
33
34
  parser.add_argument("--no-cache", action="store_true", help="跳过缓存")
34
35
  parser.add_argument("--mode", default="fast", choices=["fast", "auto", "deep", "budget"],
35
36
  help="unified-search 模式透传")
37
+ parser.add_argument("--since", default=None,
38
+ help="发布时间下限(7d / 2026-08-01),下推到支持时间参数的引擎")
39
+ parser.add_argument("--until", default=None,
40
+ help="发布时间上限(7d / 2026-08-01),下推到支持时间参数的引擎")
41
+ parser.add_argument("--sort", default="relevance",
42
+ choices=["relevance", "oldest", "newest"],
43
+ help="时间排序:relevance=相关度(默认), oldest=最早在前(溯源), newest=最新在前")
36
44
  parser.add_argument("--json", action="store_true", help="输出 JSON")
37
45
  args = parser.parse_args()
38
46
 
@@ -48,6 +56,9 @@ def main():
48
56
  max_parallel=args.max_parallel,
49
57
  skip_cache=args.no_cache,
50
58
  mode=args.mode,
59
+ since=args.since,
60
+ until=args.until,
61
+ sort=args.sort,
51
62
  )
52
63
  print(json.dumps(result, ensure_ascii=False, indent=2))
53
64
 
@@ -15,51 +15,11 @@ html:
15
15
  url_attr: href
16
16
  score: 0.75
17
17
 
18
- local_google:
19
- container: "div.g"
20
- title: "h3"
21
- url: "a"
22
- snippet: "div.VwiC3b, div.s, span.aCOpRe"
23
- url_attr: href
24
- score: 0.75
25
-
26
- local_mojeek:
27
- container: "li.results-standard-list"
28
- title: "a.ob"
29
- url: "a.ob"
30
- snippet: "p.s"
31
- url_attr: href
32
- score: 0.7
33
-
34
- local_yandex:
35
- container: "li.serp-item"
36
- title: "a.Link"
37
- url: "a.Link"
38
- snippet: "div.TextContainer"
39
- url_attr: href
40
- score: 0.65
41
-
42
- local_startpage:
43
- container: "div.result"
44
- title: "a.result-title"
45
- url: "a.result-title"
46
- snippet: "p.result-snippet"
47
- url_attr: href
48
- score: 0.7
49
-
50
- local_duckduckgo:
51
- container: ".result"
52
- title: "a.result__a"
53
- url: "a.result__a"
54
- snippet: "a.result__snippet"
55
- url_attr: href
56
- score: 0.7
57
-
58
18
  local_baidu:
59
- container: ".result, .c-container"
19
+ container: ".result, .c-container, .cosc-card-content-border"
60
20
  title: "h3"
61
21
  url: "a"
62
- snippet: ".content-right_8Zs40, .c-abstract, .content-left_3d_H4"
22
+ snippet: ".c-abstract, .cosc-card-content-border, .cosc-card-content, .summary-text_15QGa, .text_2NOr6, .main-info_4Q_kj, .bk_polysemy_1Ef6j"
63
23
  url_attr: href
64
24
  score: 0.7
65
25
 
@@ -71,6 +31,22 @@ html:
71
31
  url_attr: href
72
32
  score: 0.65
73
33
 
34
+ local_360:
35
+ container: "li.res-list"
36
+ title: "h3.res-title a"
37
+ url: "h3.res-title a"
38
+ snippet: "p.res-desc"
39
+ url_attr: data-mdurl
40
+ score: 0.7
41
+
42
+ local_jisilu:
43
+ container: ".aw-question-list .aw-item"
44
+ title: "h4 a"
45
+ url: "h4 a"
46
+ snippet: ".aw-text-color-999"
47
+ url_attr: href
48
+ score: 0.6
49
+
74
50
  local_stackoverflow:
75
51
  container: ".question-summary"
76
52
  title: "a.question-hyperlink"
@@ -95,12 +71,28 @@ html:
95
71
  url_attr: href
96
72
  score: 0.7
97
73
 
74
+ know_your_meme:
75
+ container: "a.item"
76
+ title: "h3"
77
+ url: "a"
78
+ url_attr: href
79
+ score: 0.7
80
+
81
+ moegirl:
82
+ container: "li.mw-search-result"
83
+ title: "div.mw-search-result-heading a"
84
+ url: "div.mw-search-result-heading a"
85
+ snippet: "div.searchresult"
86
+ url_attr: href
87
+ score: 0.7
88
+
98
89
  xml:
99
90
  local_arxiv:
100
91
  entry_path: ".//{http://www.w3.org/2005/Atom}entry"
101
92
  title: "atom:title"
102
93
  url: "atom:id"
103
94
  snippet: "atom:summary"
95
+ published_at: "atom:published"
104
96
  namespaces:
105
97
  atom: "http://www.w3.org/2005/Atom"
106
98
 
@@ -138,36 +130,42 @@ json:
138
130
  title: "full_name"
139
131
  url: "html_url"
140
132
  snippet: "description"
133
+ published_at: "created_at"
141
134
 
142
135
  local_npm:
143
136
  items: "objects"
144
137
  title: "package.name"
145
138
  url: "package.links.npm"
146
139
  snippet: "package.description"
140
+ published_at: "package.date"
147
141
 
148
142
  local_gitlab:
149
143
  items: "."
150
144
  title: "name_with_namespace"
151
145
  url: "web_url"
152
146
  snippet: "description"
147
+ published_at: "created_at"
153
148
 
154
149
  local_stackoverflow:
155
150
  items: "items"
156
151
  title: "title"
157
152
  url: "link"
158
153
  snippet: null
154
+ published_at: "creation_date"
159
155
 
160
156
  local_crossref:
161
157
  items: "message.items"
162
158
  title: "title"
163
159
  url: "URL"
164
160
  snippet: "abstract"
161
+ published_at: "message.published-print.date-parts.0"
165
162
 
166
163
  local_semantic_scholar:
167
164
  items: "data"
168
165
  title: "title"
169
166
  url: "url"
170
167
  snippet: "abstract"
168
+ published_at: "publicationDate"
171
169
 
172
170
  local_pubmed:
173
171
  items: "esearchresult.idlist"