argo-search 1.0.1 → 2.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +822 -0
  3. package/README.es.md +864 -0
  4. package/README.ja.md +864 -0
  5. package/README.ko.md +864 -0
  6. package/README.md +867 -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/settings-keys.png +0 -0
  12. package/assets/readme/settings-tools.png +0 -0
  13. package/assets/readme/why-better.svg +104 -0
  14. package/assets/readme/workflow.svg +83 -0
  15. package/backends/domain_profiles.json +2308 -96
  16. package/backends/engine_registry.yaml +3314 -503
  17. package/backends/query_synonyms_cn.json +35 -0
  18. package/backends/quota_profiles.json +2409 -161
  19. package/backends/source_types_cn.json +83 -0
  20. package/backends/text_lexicon.json +44 -0
  21. package/bin/argo +703 -0
  22. package/bin/argo.js +71 -39
  23. package/config.yaml +4976 -363
  24. package/cordis.patch.yml +21 -0
  25. package/data/countries.json +40 -0
  26. package/data/lang_matrix/lang_capability.json +842 -0
  27. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  28. package/data/regions_cn.json +1 -0
  29. package/docs/ADDING_NEW_ENGINE.md +184 -0
  30. package/docs/ARGO_INTRO.md +124 -0
  31. package/docs/DATA_SOURCE_GAPS.md +81 -0
  32. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  33. package/docs/ENGINE_CATALOG.md +564 -0
  34. package/docs/MCP_SETUP.md +106 -0
  35. package/docs/PLATFORM_PATHS.md +111 -0
  36. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  37. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  38. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  39. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  40. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  41. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  42. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  43. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  44. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  45. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  46. package/docs/SEARCH_ARCHIVE.md +115 -0
  47. package/docs/assets/architecture.svg +104 -0
  48. package/docs/assets/hero.svg +98 -0
  49. package/docs/engines/bangumi.md +27 -0
  50. package/docs/engines/biorxiv.md +27 -0
  51. package/docs/engines/clawhub.md +27 -0
  52. package/docs/engines/crt_sh.md +27 -0
  53. package/docs/engines/datacite.md +27 -0
  54. package/docs/engines/deps_dev.md +27 -0
  55. package/docs/engines/douban_movie.md +27 -0
  56. package/docs/engines/endoflife.md +27 -0
  57. package/docs/engines/flk_law.md +27 -0
  58. package/docs/engines/fxtwitter.md +27 -0
  59. package/docs/engines/google_news.md +27 -0
  60. package/docs/engines/gov_regulations.md +27 -0
  61. package/docs/engines/guqian.md +27 -0
  62. package/docs/engines/hackernews.md +27 -0
  63. package/docs/engines/iplant.md +27 -0
  64. package/docs/engines/keenable.md +27 -0
  65. package/docs/engines/met_museum.md +27 -0
  66. package/docs/engines/nasa_images.md +27 -0
  67. package/docs/engines/netease_music.md +27 -0
  68. package/docs/engines/npm.md +27 -0
  69. package/docs/engines/nvd.md +27 -0
  70. package/docs/engines/openreview.md +27 -0
  71. package/docs/engines/openstd.md +27 -0
  72. package/docs/engines/parallel_free.md +27 -0
  73. package/docs/engines/people_daily.md +27 -0
  74. package/docs/engines/realtime_index.md +27 -0
  75. package/docs/engines/sec_edgar.md +27 -0
  76. package/docs/engines/seltz.md +27 -0
  77. package/docs/engines/skillsmp.md +27 -0
  78. package/docs/engines/sspai.md +27 -0
  79. package/docs/engines/std_samr.md +27 -0
  80. package/docs/engines/train.md +27 -0
  81. package/docs/engines/un_comtrade.md +27 -0
  82. package/docs/engines/wikisource.md +27 -0
  83. package/docs/engines/zdic.md +27 -0
  84. package/docs/engines/zenodo.md +27 -0
  85. package/docs/engines/zhihu_hot_app.md +27 -0
  86. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  87. package/engines/README.md +10 -0
  88. package/engines/_template_cli.yaml +39 -0
  89. package/engines/_template_http.yaml +37 -0
  90. package/engines/plugins/_template_plugin.py +50 -0
  91. package/engines/specs/_cisa_kev.yaml +54 -0
  92. package/engines/specs/_osv.yaml +63 -0
  93. package/engines/specs/aljazeera.yaml +43 -0
  94. package/engines/specs/antara.yaml +43 -0
  95. package/engines/specs/artic.yaml +20 -0
  96. package/engines/specs/aviation_weather.yaml +39 -0
  97. package/engines/specs/bangumi.yaml +21 -0
  98. package/engines/specs/biorxiv.yaml +24 -0
  99. package/engines/specs/carbon_intensity.yaml +32 -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/deps_dev.yaml +21 -0
  113. package/engines/specs/digup.yaml +36 -0
  114. package/engines/specs/doi.yaml +39 -0
  115. package/engines/specs/douban_movie.yaml +22 -0
  116. package/engines/specs/dw_news.yaml +47 -0
  117. package/engines/specs/egov_law.yaml +21 -0
  118. package/engines/specs/elpais.yaml +43 -0
  119. package/engines/specs/endoflife.yaml +21 -0
  120. package/engines/specs/energy_charts.yaml +21 -0
  121. package/engines/specs/factcheck_org.yaml +52 -0
  122. package/engines/specs/faz.yaml +43 -0
  123. package/engines/specs/federal_register.yaml +57 -0
  124. package/engines/specs/firecrawl.yaml +53 -0
  125. package/engines/specs/flk_law.yaml +24 -0
  126. package/engines/specs/folha.yaml +43 -0
  127. package/engines/specs/france24.yaml +44 -0
  128. package/engines/specs/full_fact.yaml +47 -0
  129. package/engines/specs/fxtwitter.yaml +31 -0
  130. package/engines/specs/gbfs_nyc.yaml +32 -0
  131. package/engines/specs/gdacs.yaml +21 -0
  132. package/engines/specs/gdelt.yaml +64 -0
  133. package/engines/specs/google_news.yaml +22 -0
  134. package/engines/specs/gov_regulations.yaml +38 -0
  135. package/engines/specs/guardian_rss.yaml +59 -0
  136. package/engines/specs/hurriyet.yaml +43 -0
  137. package/engines/specs/iplant.yaml +25 -0
  138. package/engines/specs/jikan.yaml +24 -0
  139. package/engines/specs/jolpica.yaml +20 -0
  140. package/engines/specs/k10plus.yaml +20 -0
  141. package/engines/specs/keenable.yaml +44 -0
  142. package/engines/specs/lefigaro.yaml +43 -0
  143. package/engines/specs/listenbrainz.yaml +19 -0
  144. package/engines/specs/matichon.yaml +43 -0
  145. package/engines/specs/met_museum.yaml +21 -0
  146. package/engines/specs/nasa_images.yaml +45 -0
  147. package/engines/specs/netease_music.yaml +38 -0
  148. package/engines/specs/nhk.yaml +43 -0
  149. package/engines/specs/nhtsa_vpic.yaml +20 -0
  150. package/engines/specs/noaa_swpc.yaml +20 -0
  151. package/engines/specs/nvd.yaml +38 -0
  152. package/engines/specs/obis.yaml +20 -0
  153. package/engines/specs/opencitations.yaml +52 -0
  154. package/engines/specs/openf1.yaml +21 -0
  155. package/engines/specs/openfoodfacts.yaml +45 -0
  156. package/engines/specs/openligadb.yaml +20 -0
  157. package/engines/specs/openreview.yaml +37 -0
  158. package/engines/specs/openstd.yaml +21 -0
  159. package/engines/specs/people_daily.yaml +22 -0
  160. package/engines/specs/realtime_index.yaml +34 -0
  161. package/engines/specs/ror.yaml +20 -0
  162. package/engines/specs/satnogs.yaml +20 -0
  163. package/engines/specs/sec_edgar.yaml +46 -0
  164. package/engines/specs/skillsmp.yaml +50 -0
  165. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  166. package/engines/specs/soilgrids.yaml +25 -0
  167. package/engines/specs/sspai.yaml +36 -0
  168. package/engines/specs/stackexchange.yaml +46 -0
  169. package/engines/specs/std_samr.yaml +21 -0
  170. package/engines/specs/tass.yaml +43 -0
  171. package/engines/specs/tle_mirror.yaml +21 -0
  172. package/engines/specs/train.yaml +37 -0
  173. package/engines/specs/tvmaze.yaml +20 -0
  174. package/engines/specs/twitter_syndication.yaml +31 -0
  175. package/engines/specs/un_comtrade.yaml +23 -0
  176. package/engines/specs/unpaywall.yaml +54 -0
  177. package/engines/specs/vnpress.yaml +43 -0
  178. package/engines/specs/weather.yaml +32 -0
  179. package/engines/specs/who_don.yaml +21 -0
  180. package/engines/specs/who_gho.yaml +20 -0
  181. package/engines/specs/wikimedia_commons.yaml +34 -0
  182. package/engines/specs/wikisource.yaml +22 -0
  183. package/engines/specs/worms.yaml +19 -0
  184. package/engines/specs/x_trending.yaml +34 -0
  185. package/engines/specs/yna.yaml +43 -0
  186. package/engines/specs/ynet.yaml +43 -0
  187. package/engines/specs/zbmath.yaml +40 -0
  188. package/engines/specs/zdic.yaml +22 -0
  189. package/engines/specs/zenodo.yaml +27 -0
  190. package/engines/specs/zhihu_hot_app.yaml +35 -0
  191. package/mcp/clients.yaml +74 -0
  192. package/package.json +51 -12
  193. package/packages/dsh-plugin/README.md +134 -0
  194. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  195. package/packages/dsh-plugin/dsh/client.js +1244 -0
  196. package/packages/dsh-plugin/dsh/index.js +2290 -0
  197. package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
  198. package/packages/dsh-plugin/package.json +38 -0
  199. package/packages/dsh-plugin/test/client.test.js +232 -0
  200. package/packages/dsh-plugin/test/config.test.js +30 -0
  201. package/packages/dsh-plugin/test/index.test.js +1338 -0
  202. package/references/query-templates.md +96 -0
  203. package/references/query-templates.yaml +118 -0
  204. package/scripts/ab_eval.py +219 -0
  205. package/scripts/ab_eval_p0p1.py +266 -0
  206. package/scripts/adaptive.py +271 -70
  207. package/scripts/answer.py +154 -0
  208. package/scripts/archive_run.py +591 -0
  209. package/scripts/argo_engine_registry.py +172 -0
  210. package/scripts/argo_paths.py +728 -0
  211. package/scripts/article.py +136 -0
  212. package/scripts/arxiv_source.py +201 -0
  213. package/scripts/badcase_ledger.py +252 -0
  214. package/scripts/batch_probe.py +241 -0
  215. package/scripts/benchmark.py +19 -0
  216. package/scripts/bounded_run.py +144 -0
  217. package/scripts/browser_auth.py +323 -0
  218. package/scripts/burstiness_detector.py +147 -0
  219. package/scripts/cache.py +1078 -67
  220. package/scripts/cache_guard.py +271 -0
  221. package/scripts/cache_key_vdom.py +56 -0
  222. package/scripts/cache_similarity.py +170 -0
  223. package/scripts/caliber_isolation.py +195 -0
  224. package/scripts/calibrate_lowquality.py +210 -0
  225. package/scripts/candidate_envelope.py +366 -0
  226. package/scripts/chrome_cdp.py +871 -0
  227. package/scripts/circuit_breaker.py +389 -0
  228. package/scripts/citations.py +334 -0
  229. package/scripts/cite_cli.py +59 -0
  230. package/scripts/cjk_tokens.py +95 -0
  231. package/scripts/clarify.py +47 -24
  232. package/scripts/cli_io.py +240 -0
  233. package/scripts/config.py +683 -45
  234. package/scripts/config_cache.py +402 -0
  235. package/scripts/conn_pool.py +220 -0
  236. package/scripts/content_security.py +685 -0
  237. package/scripts/content_signals.py +730 -0
  238. package/scripts/crawl.py +74 -35
  239. package/scripts/daemon.py +866 -0
  240. package/scripts/digup_bridge.py +214 -0
  241. package/scripts/engine_admission.py +366 -0
  242. package/scripts/engine_dispatch.py +1019 -0
  243. package/scripts/engine_env.py +534 -0
  244. package/scripts/engine_failure.py +411 -0
  245. package/scripts/engine_families.py +732 -0
  246. package/scripts/engine_policy.py +218 -0
  247. package/scripts/engine_requires.py +190 -0
  248. package/scripts/engine_status.py +322 -0
  249. package/scripts/engine_validate.py +638 -0
  250. package/scripts/engines.py +778 -294
  251. package/scripts/engines_base.py +1521 -0
  252. package/scripts/engines_builders.py +298 -0
  253. package/scripts/engines_builders_batch10.py +159 -0
  254. package/scripts/engines_builders_batch11.py +174 -0
  255. package/scripts/engines_builders_batch12.py +103 -0
  256. package/scripts/engines_builders_batch13.py +340 -0
  257. package/scripts/engines_builders_batch9.py +1468 -0
  258. package/scripts/engines_builders_cn.py +2283 -0
  259. package/scripts/engines_builders_data.py +2923 -0
  260. package/scripts/engines_builders_data_macro.py +775 -0
  261. package/scripts/engines_builders_feeds.py +255 -0
  262. package/scripts/engines_builders_intl.py +1239 -0
  263. package/scripts/engines_builders_search.py +440 -0
  264. package/scripts/engines_builders_tech.py +1360 -0
  265. package/scripts/evidence.py +588 -162
  266. package/scripts/evidence_loop.py +592 -0
  267. package/scripts/evidence_tier.py +298 -0
  268. package/scripts/except_sets.py +25 -0
  269. package/scripts/extract.py +55 -16
  270. package/scripts/fact_align.py +207 -0
  271. package/scripts/fact_crosscheck.py +230 -0
  272. package/scripts/fetch.py +25 -12
  273. package/scripts/fetch_browser.py +87 -0
  274. package/scripts/fetch_jsrun.py +122 -0
  275. package/scripts/fetch_quality.py +225 -0
  276. package/scripts/fetch_render_tinyfish.py +117 -0
  277. package/scripts/fetch_v3.py +1779 -0
  278. package/scripts/focus_extract.py +350 -0
  279. package/scripts/fulltext_store.py +169 -0
  280. package/scripts/fxtwitter_api.py +325 -0
  281. package/scripts/gen_engine_catalog.py +364 -0
  282. package/scripts/gen_native_tools.py +91 -0
  283. package/scripts/gen_routing_snapshot.py +184 -0
  284. package/scripts/health_check.py +140 -402
  285. package/scripts/health_probe.py +23 -8
  286. package/scripts/hot_state.py +134 -0
  287. package/scripts/http_client.py +1053 -0
  288. package/scripts/image_ops.py +430 -0
  289. package/scripts/image_vision/vision_probe +0 -0
  290. package/scripts/image_vision/vision_probe.swift +126 -0
  291. package/scripts/install.ps1 +148 -0
  292. package/scripts/install.sh +206 -0
  293. package/scripts/job.py +1186 -0
  294. package/scripts/jump_resolver.py +235 -0
  295. package/scripts/lang_capability.py +150 -0
  296. package/scripts/lang_detect.py +289 -0
  297. package/scripts/lang_matrix_probe.py +329 -0
  298. package/scripts/lang_pref.py +311 -0
  299. package/scripts/link_source.py +340 -0
  300. package/scripts/local_engines.py +321 -0
  301. package/scripts/local_image.py +970 -0
  302. package/scripts/local_seek.py +366 -0
  303. package/scripts/macro_countries.py +82 -0
  304. package/scripts/matrix_search_eval.py +1105 -0
  305. package/scripts/mcp_diag.py +8 -3
  306. package/scripts/mcp_handlers.py +1074 -0
  307. package/scripts/mcp_handlers_surface.py +157 -0
  308. package/scripts/mcp_launch.sh +122 -0
  309. package/scripts/mcp_server.py +118 -417
  310. package/scripts/mcp_setup.py +407 -0
  311. package/scripts/mcp_tools.py +330 -0
  312. package/scripts/mcp_transport.py +191 -0
  313. package/scripts/min_version_scan.py +335 -0
  314. package/scripts/net_proxy.py +258 -0
  315. package/scripts/network_aware.py +152 -0
  316. package/scripts/paper.py +274 -0
  317. package/scripts/pdf_extract.py +518 -0
  318. package/scripts/phrase_filter.py +66 -0
  319. package/scripts/plan.py +422 -0
  320. package/scripts/postprocess_signals.py +150 -0
  321. package/scripts/pre-commit +48 -0
  322. package/scripts/query_enhance.py +157 -0
  323. package/scripts/query_operators.py +196 -0
  324. package/scripts/query_rewriter.py +116 -7
  325. package/scripts/query_signals.py +300 -0
  326. package/scripts/query_templates.py +167 -0
  327. package/scripts/query_understanding.py +411 -0
  328. package/scripts/query_variants.py +298 -0
  329. package/scripts/quota.py +488 -76
  330. package/scripts/rank_signals.py +775 -0
  331. package/scripts/ranking_eval.py +319 -0
  332. package/scripts/readability_extract.py +433 -0
  333. package/scripts/recompute.py +415 -0
  334. package/scripts/recovery.py +616 -0
  335. package/scripts/recovery_l0.py +160 -0
  336. package/scripts/redskill/redskill_engine.py +332 -0
  337. package/scripts/regression_p0p1.py +318 -0
  338. package/scripts/replay_eval.py +640 -0
  339. package/scripts/rerank_tfidf.py +225 -0
  340. package/scripts/research.py +591 -403
  341. package/scripts/research_cli.py +361 -0
  342. package/scripts/research_dossier.py +492 -0
  343. package/scripts/research_expand.py +146 -0
  344. package/scripts/research_gates.py +272 -0
  345. package/scripts/research_report.py +209 -0
  346. package/scripts/research_strategy.py +37 -0
  347. package/scripts/research_work_packages.py +195 -0
  348. package/scripts/result_lang.py +311 -0
  349. package/scripts/robots_guard.py +259 -0
  350. package/scripts/route.py +921 -254
  351. package/scripts/route_bangs.py +40 -0
  352. package/scripts/route_cache.py +225 -0
  353. package/scripts/route_combo.py +910 -0
  354. package/scripts/route_domains.py +328 -0
  355. package/scripts/route_lang.py +605 -0
  356. package/scripts/route_log.py +63 -0
  357. package/scripts/route_policy.py +250 -0
  358. package/scripts/rrf_classic.py +66 -0
  359. package/scripts/screenshot.py +73 -0
  360. package/scripts/search.py +897 -405
  361. package/scripts/search_attribution.py +88 -0
  362. package/scripts/search_benchmark.py +407 -0
  363. package/scripts/search_cli.py +627 -0
  364. package/scripts/search_entry.py +389 -0
  365. package/scripts/search_output.py +606 -0
  366. package/scripts/search_pipeline.py +1077 -0
  367. package/scripts/search_rank.py +991 -0
  368. package/scripts/search_types.py +27 -12
  369. package/scripts/seek_locator.py +68 -0
  370. package/scripts/semantic_evidence.py +215 -0
  371. package/scripts/serp_guard.py +228 -0
  372. package/scripts/serp_kill_log.py +51 -0
  373. package/scripts/single_flight.py +120 -0
  374. package/scripts/social_engines/__init__.py +10 -0
  375. package/scripts/social_engines/bilibili_engine.py +131 -0
  376. package/scripts/social_engines/hackernews_engine.py +113 -0
  377. package/scripts/social_engines/reddit_engine.py +172 -0
  378. package/scripts/social_engines/twitter_engine.py +397 -0
  379. package/scripts/social_engines/v2ex_engine.py +101 -0
  380. package/scripts/social_engines/weibo_engine.py +128 -0
  381. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  382. package/scripts/social_engines/zhihu_engine.py +139 -0
  383. package/scripts/social_research.py +201 -0
  384. package/scripts/sogou_weixin_browser.py +318 -0
  385. package/scripts/source_ledger.py +249 -0
  386. package/scripts/stage_timing.py +58 -0
  387. package/scripts/stats_cli.py +95 -0
  388. package/scripts/straggler_policy.py +60 -0
  389. package/scripts/stylometry_detector.py +231 -0
  390. package/scripts/sync_backends.py +353 -0
  391. package/scripts/sync_installs.py +29 -0
  392. package/scripts/template_cli.py +127 -0
  393. package/scripts/text_lexicon.py +72 -0
  394. package/scripts/tfidf_router.py +109 -37
  395. package/scripts/time_utils.py +233 -0
  396. package/scripts/topic_research_profiles.py +643 -0
  397. package/scripts/train.py +737 -0
  398. package/scripts/tweet.py +596 -0
  399. package/scripts/url_canon.py +217 -0
  400. package/scripts/url_optimize.py +56 -0
  401. package/scripts/url_safety.py +236 -0
  402. package/scripts/usage_log.py +116 -0
  403. package/scripts/usage_log_clean.py +113 -0
  404. package/scripts/v2ex_nodes.py +311 -0
  405. package/scripts/variant_recall.py +188 -0
  406. package/scripts/watch.py +205 -0
  407. package/scripts/wechat_article.py +258 -0
  408. package/scripts/wide_research.py +402 -0
  409. package/scripts/wx.py +349 -0
  410. package/scripts/x_article_fetch.py +141 -0
  411. package/scripts/x_search.py +234 -0
  412. package/scripts/x_trending.py +189 -0
  413. package/scripts/yaml_load.py +67 -0
  414. package/sub-skills/ego-search/SKILL.md +253 -0
  415. package/sub-skills/ego-search/references/browser-runtime.md +43 -0
  416. package/sub-skills/ego-search/references/install.md +67 -0
  417. package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
  418. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  419. package/sub-skills/ego-search/scripts/merge.py +226 -0
  420. package/sub-skills/ego-search/scripts/quality.py +64 -0
  421. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  422. package/sub-skills/ego-search/scripts/safety.py +109 -0
  423. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  424. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  425. package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
  426. package/sub-skills/js-run/SKILL.md +67 -0
  427. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  428. package/sub-skills/js-run/scripts/shim.js +188 -0
  429. package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
  430. package/sub-skills/js-run/tests/test_jsrun.py +165 -0
  431. package/sub-skills/local-search/SKILL.md +50 -53
  432. package/sub-skills/local-search/bm25.py +162 -0
  433. package/sub-skills/local-search/config.yaml +152 -55
  434. package/sub-skills/local-search/engine_registry.py +91 -26
  435. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  436. package/sub-skills/local-search/local_search_adapter.py +13 -2
  437. package/sub-skills/local-search/parse_maps.yaml +40 -42
  438. package/sub-skills/local-search/search_v3.py +1182 -65
  439. package/sub-skills/local-search/smart_router.py +47 -7
  440. package/sub-skills/local-seek/SKILL.md +110 -0
  441. package/sub-skills/local-seek/config/domains.yaml +20 -0
  442. package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
  443. package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
  444. package/sub-skills/local-seek/references/git-integration.md +54 -0
  445. package/sub-skills/local-seek/references/remote-handoff.md +46 -0
  446. package/sub-skills/local-seek/references/rg-recipes.md +96 -0
  447. package/sub-skills/local-seek/references/strategies.md +52 -0
  448. package/sub-skills/local-seek/references/structural-search.md +59 -0
  449. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  450. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  451. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  452. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  453. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  454. package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
  455. package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
  456. package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
  457. package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
  458. package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
  459. package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
  460. package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
  461. package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
  462. package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
  463. package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env python3
2
+ """config_cache.py — 配置的跨进程磁盘缓存(性能层,可整体关闭)。
3
+
4
+ ## 为什么单独成模块
5
+
6
+ 配置加载有两条完全不同的路径:
7
+
8
+ 1. **正确性路径**(config.py):解析 config.yaml + 合并 60 多个外置声明,产物
9
+ 被当作**事实**(db_path、引擎表…),必须逐字节正确;
10
+ 2. **性能路径**(本模块):把上面那次解析的结果按 (mtime, size, ctime, 内容摘要)
11
+ 存成 JSON,让下一个进程省掉整笔解析(实测 50–82 ms → 16–18 ms)。
12
+
13
+ 两者的失效判据、schema 版本、清理策略都是独立的——混在一个文件里时,改缓存逻辑
14
+ 要先在 1000 行配置代码里找到「跨进程配置缓存」那一段。这里的产物只是**优化结果**:
15
+ 缓存错了最坏是多解析一次,绝不改变语义(判据见 `_config_content_digest` 的说明)。
16
+
17
+ `ARGO_CONFIG_DISK_CACHE=0` 可整体关闭;只读环境、schema 不匹配、摘要不可用时
18
+ 一律 fail-open 回落到解析路径。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import json
25
+ import os
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from except_sets import OPT_IMPORT, SHAPE_BENIGN
30
+
31
+ # 状态目录唯一来源。argo_paths 只在函数体内反向 import config,此处模块级导入
32
+ # 不会成环(与 config.py 的同一处理,见那边的注释)。
33
+ import argo_paths # noqa: E402
34
+
35
+
36
+ def _config_path() -> Path:
37
+ """config.yaml 路径:**延迟**从 config 取(config 在模块级导入本模块,成环)。"""
38
+ from config import CONFIG_PATH
39
+ return CONFIG_PATH
40
+
41
+
42
+ def _local_config_path() -> Path:
43
+ """config.local.yaml 路径:同样延迟取(与 _config_path 同一成环约束)。"""
44
+ from config import LOCAL_CONFIG_PATH
45
+ return LOCAL_CONFIG_PATH
46
+
47
+
48
+ def _local_cache_db_path() -> str | None:
49
+ """本机覆盖层里的 cache.db_path(没有则 None)。
50
+
51
+ db_path 是状态目录的源头,本机覆盖层**可以**改它;peek 只读缓存里的公共
52
+ 值会让 load_config 与 argo_paths 算出两个状态目录(见 _peek_disk_cache_db_path
53
+ 的说明)。这里自包含地读那个小文件,刻意不依赖 config 的帮助函数——peek 会
54
+ 在 config 模块初始化早期(DEFAULT_CONFIG 的 db_path)被 argo_paths 调到,
55
+ 那时 config 的 _load_local_config 还没定义。
56
+ """
57
+ try:
58
+ path = _local_config_path()
59
+ if not path.is_file():
60
+ return None
61
+ from yaml_load import loads
62
+ data = loads(path.read_text(encoding="utf-8")) or {}
63
+ except Exception:
64
+ return None
65
+ if not isinstance(data, dict):
66
+ return None
67
+ cache_cfg = data.get("cache")
68
+ if not isinstance(cache_cfg, dict):
69
+ return None
70
+ db_path = cache_cfg.get("db_path")
71
+ return os.path.expanduser(str(db_path)) if db_path else None
72
+
73
+
74
+ # ── 跨进程配置缓存(写入文件 JSON)─────────────────────────────────────────────────
75
+ #
76
+ # 为什么需要:每条命令都是一次新进程,config.yaml + 63 个外置声明的解析合并
77
+ # 在**每个** CLI 调用里重付一遍;进程内记忆化救不了跨进程的重复。输入几乎从不
78
+ # 变化,故把「归一化后」的配置按输入指纹写入文件,命中时用 json.loads 取代整条链。
79
+ # 实测(新进程 min of 4,macOS/Python 3.14):load_config 关缓存 50–82 ms、命中
80
+ # 16–18 ms;其中 read+json.loads 约 2 ms,余下是每次都必须现算的外置声明扫描与
81
+ # CLI 路径校验(那两项是「结论随环境变,不能连结论一起缓存」的部分)。
82
+ #
83
+ # 键必须同时覆盖「输入」与「解释输入的代码」:只按 config.yaml 的 mtime 命中,
84
+ # 升级 argo(归一化逻辑改了)后仍会读回旧逻辑写下的缓存,表现是「升级了没生效」。
85
+ # 故把 config.py 与 yaml_load.py 的源码 mtime 也算进键里(见 _loader_sig)。
86
+ _CONFIG_DISK_CACHE_SCHEMA = 3
87
+ _loader_sig_cache: int | None = None
88
+
89
+
90
+ def _loader_sig() -> int:
91
+ """本模块源码的 mtime_ns —— 磁盘缓存的「代码版本」键(进程内记忆一次)。
92
+
93
+ 归一化链不只在 config.py:`_load_yaml` 走 yaml_load.py(loader 选择直接
94
+ 影响解析结果)。两处都进签名,改任一处缓存立刻失效——不这么做的话,
95
+ 升级后读回的是旧逻辑写下的结果,表现是「改了没生效」。
96
+ """
97
+ global _loader_sig_cache
98
+ if _loader_sig_cache is None:
99
+ sig = 0
100
+ for mod in (Path(__file__), Path(__file__).parent / "yaml_load.py"):
101
+ try:
102
+ sig = (sig * 1_000_003 + mod.stat().st_mtime_ns) % (2 ** 62)
103
+ except OSError:
104
+ continue
105
+ _loader_sig_cache = sig
106
+ return _loader_sig_cache
107
+
108
+
109
+ def _config_disk_cache_enabled() -> bool:
110
+ """写入文件缓存开关(默认开);ARGO_CONFIG_CACHE=0/false/no/off 关闭。
111
+
112
+ 走 engine_env.env_flag 而不是自己判真假:全仓布尔开关只有这一套计算方式(它
113
+ 同时认 env 文件里的写法),再写一份 `in {"0","false",...}` 就是又一处会
114
+ 漂移的第二计算方式。engine_env 只依赖标准库,懒导入避免与 config 成环。
115
+ """
116
+ try:
117
+ from engine_env import env_flag
118
+ return env_flag("ARGO_CONFIG_CACHE", default=True)
119
+ except OPT_IMPORT + SHAPE_BENIGN:
120
+ # 判定链不可用时按「开」处理:缓存是纯性能优化,关掉它不影响正确性,
121
+ # 而误判为「关」只会让每次调用回到全量解析(旧的既定行为)。
122
+ return True
123
+
124
+
125
+ def _config_disk_cache_path() -> Path:
126
+ """写入文件缓存位置:**引导根目录**,按 config 路径分槽,不依赖配置解析。
127
+
128
+ 这是解开「自锁」的关键:状态目录(argo_paths.state_root)要靠 config.yaml
129
+ 的 cache.db_path 才能算出来,而缓存的意义正是省掉这次解析——把缓存放进
130
+ 状态目录,就变成了「为了找缓存必须先解析配置」。故放在一个静态可推导的
131
+ 引导位置:ARGO_STATE_DIR(测试/多环境隔离)优先,否则历史默认根。
132
+
133
+ 文件名按 config.yaml 的绝对路径分槽:同机多份 argo(skills/argo 与一份
134
+ backup 副本、或两个不同 checkout)共用引导根时,此前会互相顶掉对方的缓存,
135
+ 每次调用都退化成全量解析——命中率归零,收益归零(审计实测 5 次运行 5 次
136
+ 全解析)。分槽后各写各的,键里的 config_path 仍留作第二道校验。
137
+ """
138
+ override = os.environ.get(argo_paths.ENV_STATE_DIR, "").strip()
139
+ root = (Path(os.path.expanduser(override)) if override
140
+ else argo_paths.legacy_root())
141
+ slot = hashlib.sha256(str(_config_path()).encode("utf-8")).hexdigest()[:16]
142
+ return root / f"config-cache-{slot}.json"
143
+
144
+
145
+ def _json_round_trip_safe(obj: Any) -> bool:
146
+ """配置能否无损过一遍 JSON:字典键必须都是字符串。
147
+
148
+ YAML 允许 `1: x` 这样的非字符串键,json.dumps 会静默写成 "1",回读时键
149
+ 类型已变——缓存不得改变语义,发现这类键就整个不写缓存(退回每次解析)。
150
+ """
151
+ if isinstance(obj, dict):
152
+ return all(isinstance(k, str) and _json_round_trip_safe(v)
153
+ for k, v in obj.items())
154
+ if isinstance(obj, list):
155
+ return all(_json_round_trip_safe(v) for v in obj)
156
+ return True
157
+
158
+
159
+ def _config_content_digest(st: os.stat_result) -> str | None:
160
+ """config.yaml 的**内容摘要**(blake2b/16 hex),进程内按 stat 记忆。
161
+
162
+ 为什么用内容而不是 stat 字段:这是跨平台正确性的分水岭。
163
+ - `st_mtime`/`st_size` 可以被 `touch -r`、`cp -p`、`tar -x` 原样还原;
164
+ - `st_ctime` 在 POSIX 是「元数据变更时间」(能兜住还原 mtime 的写法),
165
+ 但**在 Windows 是创建时间**——改写内容后它根本不变,于是同一份代码在
166
+ 两个平台上给出不同的失效保真度,Windows 上「改了不生效」会静默复现。
167
+ 内容摘要没有这个问题:两个平台都只认字节。代价是 121 KB 读 + 哈希实测
168
+ 0.47 ms,而它换来的是省下 19–35 ms/次,且决定状态目录(cache.db_path)
169
+ 的那份文件从此不可能读到旧版本。
170
+
171
+ 读不到(权限/被删)返回 None,调用方按「无摘要」处理——宁可不命中缓存,
172
+ 也不要拿一份源文件不明的摘要当命中依据。
173
+ """
174
+ global _content_digest_memo
175
+ key = _config_db_path_key(st)
176
+ if _content_digest_memo is not None and _content_digest_memo[0] == key:
177
+ return _content_digest_memo[1]
178
+ try:
179
+ digest: str | None = hashlib.blake2b(
180
+ _config_path().read_bytes(), digest_size=16).hexdigest()
181
+ except OSError:
182
+ digest = None
183
+ _content_digest_memo = (key, digest)
184
+ return digest
185
+
186
+
187
+ def _config_disk_cache_key(st: os.stat_result,
188
+ scan: tuple[float, int, int, str],
189
+ digest: str | None) -> dict[str, Any]:
190
+ """缓存指纹:凡能改变「解析结果」的输入都要进键。
191
+
192
+ - config_digest:config.yaml 的**内容摘要**。这是唯一的强判据——mtime 能被
193
+ `cp -p`/`touch -r` 还原,ctime 在 Windows 是创建时间(改写不变),只有
194
+ 内容不会骗人。摘要取不到(None)时键必然不等于任何已存载荷,等价于
195
+ 「不使用缓存」,这是刻意的保守选择。
196
+ - ext_digest:外置声明的集合摘要(每文件的相对路径/大小/mtime/ctime 聚合)。
197
+ 只取 max_mtime 对「删除声明」与「拷入旧时间戳的声明」是盲的,会让缓存继续
198
+ 给出一份「引擎删了还在 / 加了不生效」的配置。
199
+ - loader_sig:解释这份配置的代码变了(归一化逻辑改了)也要失效,
200
+ 否则升级后读回的是旧逻辑写下的结果。
201
+ - home:`~` 展开依赖 HOME,同一个 ARGO_STATE_DIR 下换 HOME 会拿到
202
+ 上一个 HOME 的展开路径。
203
+ """
204
+ return {
205
+ "config_path": str(_config_path()),
206
+ "config_digest": digest,
207
+ "config_size": st.st_size,
208
+ "ext_digest": scan[3],
209
+ "ext_files": scan[1],
210
+ "loader_sig": _loader_sig(),
211
+ "home": os.path.expanduser("~"),
212
+ }
213
+
214
+
215
+ def _config_db_path_key(st: os.stat_result) -> tuple[str, int, int, int]:
216
+ """config.yaml 的**进程内记忆键**(stat 三元 + ctime)——只用于避免同一进程
217
+ 重复读盘,不承担跨进程失效判据(那是 config_digest 的职责)。
218
+
219
+ 它自身不跨进程,也就没有平台语义问题:Windows 上 ctime 是创建时间,这里
220
+ 退化为 (路径, mtime, size) 三重判据,而进程内改写文件必然同时改 mtime。
221
+ """
222
+ return (str(_config_path()), st.st_mtime_ns, st.st_size, st.st_ctime_ns)
223
+
224
+
225
+ def _looks_like_config(cfg: Any) -> bool:
226
+ """缓存载荷的结构自检:不可信输入不得让加载链崩掉。
227
+
228
+ 缓存文件可能被外部改写、写坏、或来自结构已变的版本(schema 只防我们自己
229
+ 升格式,防不了手工编辑)。`engines` 被改成字符串这类结构损坏,会让下游
230
+ `cfg.get("engines", {}).items()` 抛 AttributeError——表现是「每条命令都崩
231
+ 且看不出跟缓存有关」,用户无从知道要删哪个文件。结构不符一律当「没有缓存」
232
+ 退回解析链(解析链随后会重写这份缓存,自愈)。
233
+ """
234
+ if not isinstance(cfg, dict):
235
+ return False
236
+ for key in ("engines", "domains", "cache", "execution", "network", "output"):
237
+ if key in cfg and not isinstance(cfg[key], (dict, list)):
238
+ return False
239
+ return True
240
+
241
+
242
+ _disk_cache_memo: tuple[tuple[str, int, int, int], dict[str, Any] | None] | None = None
243
+ _content_digest_memo: tuple[tuple[str, int, int, int], str | None] | None = None
244
+
245
+
246
+ def _disk_cache_payload() -> dict[str, Any] | None:
247
+ """读写入文件缓存文件的原始载荷(进程内按 config.yaml 的 stat 记忆)。
248
+
249
+ peek 与 load_config 都从这里取,一次进程最多读盘一次;读取失败(无缓存 /
250
+ 损坏 / 被裁剪)一律返回 None,由调用方走完整解析链。
251
+ """
252
+ global _disk_cache_memo
253
+ if not _config_disk_cache_enabled():
254
+ return None
255
+ try:
256
+ st = _config_path().stat()
257
+ except OSError:
258
+ return None
259
+ key = _config_db_path_key(st)
260
+ if _disk_cache_memo is not None and _disk_cache_memo[0] == key:
261
+ return _disk_cache_memo[1]
262
+ payload: dict[str, Any] | None = None
263
+ try:
264
+ raw = json.loads(_config_disk_cache_path().read_text(encoding="utf-8"))
265
+ if (isinstance(raw, dict)
266
+ and raw.get("schema") == _CONFIG_DISK_CACHE_SCHEMA
267
+ and isinstance(raw.get("key"), dict)
268
+ and isinstance(raw.get("config"), dict)):
269
+ payload = raw
270
+ except (OSError, ValueError):
271
+ payload = None
272
+ _disk_cache_memo = (key, payload)
273
+ return payload
274
+
275
+
276
+ def _load_config_disk_cache(st: os.stat_result,
277
+ scan: tuple[float, int, int, str],
278
+ digest: str | None) -> dict[str, Any] | None:
279
+ """取写入文件配置缓存;指纹不完全匹配或结构不对则返回 None(走完整解析链)。"""
280
+ if digest is None:
281
+ return None # 摘要取不到 → 不信任任何已存载荷(保守优先)
282
+ raw = _disk_cache_payload()
283
+ if raw is None or raw.get("key") != _config_disk_cache_key(st, scan, digest):
284
+ return None
285
+ cfg = raw.get("config")
286
+ return cfg if _looks_like_config(cfg) else None
287
+
288
+
289
+ def _peek_disk_cache_db_path(st: os.stat_result,
290
+ digest: str | None) -> tuple[bool, str | None]:
291
+ """从写入文件缓存里取 cache.db_path → (命中, db_path)。
292
+
293
+ 命中时 import 链上的路径派生(argo_paths)连 YAML 都不用解析——这是冷启动
294
+ 固定开销里最后一块可省的重复劳动:config.yaml 有 121 KB,C 版 loader 解析
295
+ 一遍约 20 ms,而派生只需要里面一个标量。
296
+
297
+ **必须校验 config_digest**:db_path 是状态目录的源头(argo_paths.state_root),
298
+ 只比 config_path 会漏掉「配置改了但缓存文件还在」的窗口——peek 拿到旧路径、
299
+ load_config 拿到新路径,两处计算方式分裂,长驻进程整个生命周期都会把状态文件
300
+ 写到旧目录(审计实测复现)。用内容摘要而不是 stat 字段,是因为 stat 可被
301
+ `touch -r`/`cp -p` 还原、ctime 在 Windows 又只是创建时间(同一份代码在两个
302
+ 平台上失效保真度不同)。外置声明摘要不在此校验:db_path 只由 config.yaml
303
+ 本身决定。
304
+
305
+ **本机覆盖层**:db_path 是本机覆盖层**可以**改的键(见 config._merge_local_config),
306
+ 而缓存里存的是**公共**配置(覆盖层在缓存之后才并入,见 config.load_config)。
307
+ 所以缓存命中与否都要用 _local_cache_db_path() 覆盖一次,否则本机改了
308
+ db_path 之后,load_config 拿新路径、argo_paths 拿旧路径,两处分裂。
309
+
310
+ 返回的路径与走 YAML 时**同为展开后的形式**(~ 已展开):两条分支计算方式必须
311
+ 一致,否则同一个进程里先命中缓存、后走解析会拿到两种形态,给未来的调用者
312
+ 埋雷(当前唯一消费者 argo_paths 会再 expanduser 一次,所以现在看不出来)。
313
+ """
314
+ local_db = _local_cache_db_path()
315
+ if digest is None:
316
+ return False, local_db
317
+ raw = _disk_cache_payload()
318
+ if raw is None:
319
+ return False, local_db
320
+ key = raw.get("key") or {}
321
+ if (key.get("config_path"), key.get("config_digest")) != (
322
+ str(_config_path()), digest):
323
+ return False, local_db
324
+ cfg = raw.get("config")
325
+ if not _looks_like_config(cfg):
326
+ return False, local_db
327
+ cache_cfg = cfg.get("cache")
328
+ if not isinstance(cache_cfg, dict):
329
+ return True, local_db
330
+ db_path = cache_cfg.get("db_path")
331
+ cached = os.path.expanduser(str(db_path)) if db_path else None
332
+ return True, (local_db or cached)
333
+
334
+
335
+ def _save_config_disk_cache(st: os.stat_result, scan: tuple[float, int, int, str],
336
+ digest: str | None,
337
+ config: dict[str, Any]) -> None:
338
+ """原子写写入文件缓存;任何失败静默(只读环境退回每次解析的老路)。
339
+
340
+ 存的是**归一化后、校验前**的配置:`_validate_engine_paths` 的结论依赖
341
+ 运行环境(该机器 PATH 上有没有这个 CLI、文件在不在),必须每次现算——
342
+ 把「已禁用」的结论一起缓存,会让在 A 机器(缺某 CLI)写下的判定被 B 机器
343
+ 读成事实,正是这套配置里最忌讳的「结论当输入存」。
344
+ """
345
+ global _disk_cache_memo
346
+ if digest is None or not _config_disk_cache_enabled() \
347
+ or not _json_round_trip_safe(config):
348
+ return
349
+ payload = {
350
+ "schema": _CONFIG_DISK_CACHE_SCHEMA,
351
+ "key": _config_disk_cache_key(st, scan, digest),
352
+ "config": config,
353
+ }
354
+ path = _config_disk_cache_path()
355
+ try:
356
+ # 原子写走 argo_paths 的唯一来源(mkstemp 唯一 tmp + os.replace 的
357
+ # 正确性在一处维护,多进程并行写同一目标不会互相搬走 tmp)
358
+ argo_paths.atomic_write_json(path, payload, indent=None)
359
+ except (OSError, TypeError, ValueError):
360
+ # TypeError:YAML 里的日期/自定义类型无法进 JSON——整份跳过,不降级
361
+ # 成 default=str(那会静默改变类型语义)
362
+ _disk_cache_memo = None
363
+ return
364
+ # 刚写的这份就是本进程的最新真值,直接填入记忆,省掉一次回读
365
+ _disk_cache_memo = (_config_db_path_key(st), payload)
366
+ _sweep_orphan_cache_slots(path)
367
+
368
+
369
+ def _sweep_orphan_cache_slots(keep: Path) -> None:
370
+ """回收「config_path 已不存在」的缓存槽;任何失败静默。
371
+
372
+ why(2026-09-17 实测):引导根里 8 个槽中有 6 个属于 `/tmp` 下的临时
373
+ checkout(测试与基准跑出来的),每个约 142 KB 且永久留存,合计 1.13 MB。
374
+
375
+ 判据只能是「那份 config.yaml 还在不在」:槽按 config_path 分是**多 checkout
376
+ 安全所必需的**(见 _config_disk_cache_path 的说明),所以不能按「同一个
377
+ config 的旧摘要」来删——那会误删另一个 checkout 正在用的槽。只删 config_path
378
+ 在盘上已消失的槽,既不误伤活着的 checkout,又能自愈任何来源的临时副本。
379
+
380
+ 时机:只在写缓存时顺带扫(缓存未命中才写,属低频)。临时 checkout 自己写槽
381
+ 时就会回收上一批的遗留,所以污染源活跃时回收也随之发生。
382
+ """
383
+ if not _config_disk_cache_enabled():
384
+ return
385
+ try:
386
+ siblings = list(keep.parent.glob("config-cache-*.json"))
387
+ except OSError:
388
+ return
389
+ for path in siblings:
390
+ if path == keep:
391
+ continue
392
+ try:
393
+ payload = json.loads(path.read_text(encoding="utf-8"))
394
+ cfg_path = str(((payload.get("key") or {}).get("config_path")) or "").strip()
395
+ except (OSError, ValueError, AttributeError):
396
+ # 读不动或结构不认识:一律不动。判定不了就不删,是这里的保守选择。
397
+ continue
398
+ if cfg_path and not Path(cfg_path).exists():
399
+ try:
400
+ path.unlink()
401
+ except OSError:
402
+ continue
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ """conn_pool.py — http.client 连接池:keep-alive 复用的进程级实现。
3
+
4
+ 问题(第一性原理):argo 的 HTTP 出口此前是「一次请求一条连接」——每次请求
5
+ 重付 TCP + TLS 握手(2 RTT:国内源 ≈20–60 ms,海外源 ≈300–600 ms)。一次
6
+ 典型搜索的握手次数:2–3 个引擎各 1 次 + wave-1 对冲补发 1 次 + fetch_v3 单次
7
+ 抓取 4–6 跳(md 协商探测 / mobile UA / 主链 / TLS 伪造 / jina / archive CDX)
8
+ + `--verify` 的 3 线程 × fetch 全链 ≈ 12–18 次。即普通搜索 150–500 ms、
9
+ research/verify 路径 0.5–3 s 是纯握手开销——而同一主机连打多次时,这些握手
10
+ 完全相同,本可一次完成。
11
+
12
+ 设计约束(每条都对应一类事故):
13
+
14
+ 1. **键 = (scheme, host, port, proxy_url)**:只有同主机、同出口(直连或
15
+ 同一个代理)才可复用。HTTPS 经代理时连接上已建立 CONNECT 隧道(主机
16
+ 固定在内),换主机必须换连接。重定向跨主机时,连接归还到 **borrow
17
+ 时记下的原键**——调用方负责,本模块按传入的 key 归还。
18
+ 2. **线程安全**:MCP server 常驻 + verify 的 ThreadPoolExecutor 并发借还。
19
+ 每键一把 LifoQueue(后进先出:刚还回的热连接最可能活着)+ 一把全局锁
20
+ 护计数。
21
+ 3. **陈旧连接**:服务端 keep-alive 空闲超时(常见 5–15 s)后,复用连接的
22
+ 首个请求必失败(RemoteDisconnected / BadStatusLine)。本模块不重试
23
+ (GET 幂等,重试策略归调用方),但做三件事降低概率:借出时打时间戳,
24
+ 超过 IDLE_TTL 的连接直接关闭不借出;借出前检查 socket 未关闭;调用方
25
+ 对「复用连接的首个请求」失败应直接丢弃重借,不计入退避预算。
26
+ 4. **有界**:每键 idle 上限 + 全局 idle 上限 + idle TTL。超限还回即关闭。
27
+ 达到上限时 borrow 仍**新建无记账连接**——池是优化不是限流器,不能把
28
+ 并发卡死(早停弃置的 stray 线程也可能持连接,限流会让它们互相等)。
29
+ 5. **响应体读干才能还池**:否则下一个请求会读到上一个响应体。调用方守约
30
+ (http_client._do_get 在读干 body 后才 give_back);重定向响应未读干,
31
+ 走 discard。
32
+
33
+ 计数口径:_live[key] = 「已创建且未关闭」的连接数(idle + 借出都算),
34
+ 创建 +1、关闭 -1;idle 与借出之间流转不改计数。
35
+
36
+ 开关:ARGO_HTTP_POOL=0 时 borrow 恒新建、give_back 恒关闭——退回「一次请求
37
+ 一条连接」的旧行为,用于对拍与应急。
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import queue
43
+ import threading
44
+ import time
45
+ from typing import Any
46
+ from urllib.parse import urlparse
47
+
48
+ # 每键 idle 连接上限:覆盖同主机的少量并发(verify 3 线程 + fetch 链 4–6 跳
49
+ # 很少同主机并发超过 4)
50
+ _MAX_IDLE_PER_KEY = 4
51
+ # 全局 idle 上限:长驻进程的硬 bound,防主机数多时无界囤积
52
+ _MAX_IDLE_TOTAL = 32
53
+ # idle 连接的最长存活:超过即视为已被服务端关闭,借出前关掉
54
+ _IDLE_TTL_S = 30.0
55
+
56
+ _lock = threading.Lock()
57
+ _pools: dict[tuple, "queue.LifoQueue"] = {}
58
+ _live: dict[tuple, int] = {}
59
+ _total_idle = 0
60
+
61
+
62
+ def _pool_enabled() -> bool:
63
+ from engine_env import env_flag
64
+ # 收编到 env_flag(2026-09-29):统一真值表——n/disable/disabled/none
65
+ # 从「开」变「关」(方向是修复),且开关写进 ~/.config/argo/env 后生效
66
+ return env_flag("ARGO_HTTP_POOL", default=True)
67
+
68
+
69
+ def _default_port(scheme: str) -> int:
70
+ return 443 if scheme == "https" else 80
71
+
72
+
73
+ def key_for(parsed: Any, proxy_url: str | None) -> tuple:
74
+ """连接的复用键。parsed 为 urlparse 结果。"""
75
+ scheme = (getattr(parsed, "scheme", "") or "https").lower()
76
+ host = (getattr(parsed, "hostname", "") or "").lower()
77
+ port = getattr(parsed, "port", None) or _default_port(scheme)
78
+ return (scheme, host, port, proxy_url or "")
79
+
80
+
81
+ def key_of_url(url: str, proxy_url: str | None) -> tuple:
82
+ """便捷入口:从 URL 字符串算键(测试与调试用)。"""
83
+ from urllib.parse import urlparse as _up
84
+ return key_for(_up(url), proxy_url)
85
+
86
+
87
+ def _close(conn: Any) -> None:
88
+ try:
89
+ conn.close()
90
+ except Exception:
91
+ pass
92
+
93
+
94
+ def _sock_alive(conn: Any) -> bool:
95
+ sock = getattr(conn, "sock", None)
96
+ if sock is None:
97
+ return False
98
+ try:
99
+ return sock.fileno() != -1
100
+ except Exception:
101
+ return False
102
+
103
+
104
+ def borrow(parsed: Any, timeout: float, proxy_url: str | None,
105
+ factory: Any) -> tuple[Any, tuple, bool]:
106
+ """借一条到目标主机的连接。返回 (conn, key, reused)。
107
+
108
+ factory(parsed, timeout, proxy_url) -> conn:net_proxy.open_connection
109
+ 的签名适配(新建路径走它,保持「出口经 net_proxy 调度」这一单点)。
110
+ """
111
+ global _total_idle
112
+ key = key_for(parsed, proxy_url)
113
+ reused = None
114
+ if _pool_enabled():
115
+ with _lock:
116
+ q = _pools.get(key)
117
+ if q is not None:
118
+ while True:
119
+ try:
120
+ conn, t = q.get_nowait()
121
+ except queue.Empty:
122
+ break
123
+ _total_idle -= 1
124
+ stale = (not _sock_alive(conn)
125
+ or (time.monotonic() - t) > _IDLE_TTL_S)
126
+ if stale:
127
+ _close(conn)
128
+ _live[key] = _live.get(key, 1) - 1
129
+ continue
130
+ # idle → 借出:_live 不变(连接未关闭,只是换了状态)
131
+ reused = conn
132
+ break
133
+ if reused is not None:
134
+ # 复用连接按本次请求的 timeout 重设 socket 超时(见模块头约束 3)
135
+ sock = getattr(reused, "sock", None)
136
+ if sock is not None:
137
+ try:
138
+ sock.settimeout(timeout)
139
+ except Exception:
140
+ pass
141
+ return reused, key, True
142
+ conn = factory(parsed, timeout, proxy_url)
143
+ with _lock:
144
+ _live[key] = _live.get(key, 0) + 1
145
+ return conn, key, False
146
+
147
+
148
+ def borrow_via_net_proxy(parsed: Any, timeout: float,
149
+ proxy_url: str | None) -> tuple[Any, tuple, bool]:
150
+ """便捷入口:工厂固定为 net_proxy.open_connection。
151
+
152
+ open_connection 返回 `(conn, via_proxy)` 二元组,不能直接当工厂——
153
+ 这里包掉 via_proxy,池只拿 conn。保持「出口经 net_proxy 调度」这一
154
+ 单点:池只决定「新建还是复用」,不自己造连接。
155
+ """
156
+ def _factory(p: Any, t: float, px: str | None) -> Any:
157
+ from net_proxy import open_connection
158
+ conn, _via = open_connection(p, t, px)
159
+ return conn
160
+
161
+ return borrow(parsed, timeout, proxy_url, _factory)
162
+
163
+
164
+ def give_back(key: tuple, conn: Any) -> None:
165
+ """还回一条健康连接(响应体已读干)。超限或开关关闭时直接关闭。"""
166
+ global _total_idle
167
+ if not _pool_enabled() or not _sock_alive(conn):
168
+ discard(key, conn)
169
+ return
170
+ with _lock:
171
+ q = _pools.setdefault(key, queue.LifoQueue())
172
+ if q.qsize() >= _MAX_IDLE_PER_KEY or _total_idle >= _MAX_IDLE_TOTAL:
173
+ over = True
174
+ else:
175
+ over = False
176
+ q.put((conn, time.monotonic()))
177
+ _total_idle += 1
178
+ if over:
179
+ _close(conn)
180
+ with _lock:
181
+ if key in _live:
182
+ _live[key] -= 1
183
+ if _live[key] <= 0:
184
+ _live.pop(key, None)
185
+
186
+
187
+ def discard(key: tuple, conn: Any) -> None:
188
+ """丢弃一条连接(陈旧 / 响应体未读干 / 异常路径)。不计 idle。"""
189
+ _close(conn)
190
+ with _lock:
191
+ if key in _live:
192
+ _live[key] -= 1
193
+ if _live[key] <= 0:
194
+ _live.pop(key, None)
195
+
196
+
197
+ def stats() -> dict:
198
+ with _lock:
199
+ return {
200
+ "keys": len(_pools),
201
+ "idle": _total_idle,
202
+ "live": dict(_live),
203
+ "enabled": _pool_enabled(),
204
+ }
205
+
206
+
207
+ def clear() -> None:
208
+ """清空所有 idle 连接(测试隔离 / 进程退出前用)。"""
209
+ global _total_idle
210
+ with _lock:
211
+ for q in _pools.values():
212
+ while True:
213
+ try:
214
+ conn, _t = q.get_nowait()
215
+ except queue.Empty:
216
+ break
217
+ _close(conn)
218
+ _pools.clear()
219
+ _total_idle = 0
220
+ _live.clear()