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,234 @@
1
+ #!/usr/bin/env python3
2
+ """X (Twitter) 关键词搜索(ego-browser,纯标准库,零文件读写)。
3
+
4
+ 定位:X 原生搜索是唯一能吃 X 布尔语法的通道——`from:`(盯人)、
5
+ `-is:retweet`(去转发)、`min_faves:`(热度门)、`lang:`(语言)、
6
+ `since:`/`until:`(时间窗)。这些语法在任何通用网页搜索引擎上都会被忽略,
7
+ 所以「盯一批官方号 / 按关键词找发布」这件事只能走 x.com/search 本体。
8
+
9
+ 通道原理:借 ego-browser 驱动用户默认浏览器(Ego Lite)打开 x.com/search,
10
+ 复用浏览器 profile 的登录态与指纹。**为什么必须登录态**:X 对未登录请求
11
+ 一律拒绝或给降级页;而 x_trending 已证明该 profile 通道长期可用。
12
+
13
+ 与 fxtwitter(engines/specs/fxtwitter.yaml)的关系:那是 HTTP 免登录通道,
14
+ 曾经是主路径,但上游公开实例的 `/2/search` 已停用(对任意查询恒返回
15
+ 404 + 空 results,status/profile 端点仍正常)——本脚本是它的可用替代,
16
+ 只做关键词搜索,不做单条打包(单条走 `argo tweet`)。
17
+
18
+ 输出:YAML 列表(title/url/snippet/source/published_at/social_meta),
19
+ 与引擎统一 schema 对齐,便于被 `argo template` 直接转发。
20
+
21
+ 依赖:ego-browser CLI(Ego Lite)+ 已登录的 X 会话。缺失 / 未渲染 / 结构变化
22
+ → stderr 报因、退出码 1(调用方对 rc!=0 只取 stderr 留痕、结果为空)。
23
+
24
+ 用法:
25
+ python3 scripts/x_search.py "OpenAI" -n 20
26
+ python3 scripts/x_search.py '(GPT OR Claude) release -is:retweet' --sort top --json
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+
37
+ JS_RESULT_WAIT_S = 15 # 浏览器内等首屏渲染的墙钟(秒)
38
+ SUBPROC_TIMEOUT = 90 # ego-browser 子进程墙钟封顶(含滚动加载)
39
+ X_SEARCH_BASE = "https://x.com/search"
40
+ JSON_MARKER = "@@JSON@@"
41
+ MAX_SCROLL = 3 # 最多下滚几次以凑够 n(每次约一屏半)
42
+
43
+ # JS 侧:抓 x.com/search 渲染结果。CFG 为 JSON 字面量(ensure_ascii 注入)。
44
+ # 抽取口径全部走 data-testid,不碰 class 名(X 的 class 是哈希串,改版即失效)。
45
+ _JS_TEMPLATE = """
46
+ const CFG = %(cfg)s;
47
+ const task = await taskSpace("argo X搜索");
48
+ let page = null;
49
+ try { page = task.page("p1"); } catch (e) { page = null; }
50
+ if (!page) page = await task.newPage();
51
+ await page.goto(%(base)s + "?q=" + encodeURIComponent(CFG.query) + "&f=" + CFG.sort);
52
+ // 等首屏:出 article 视为渲染完成;出「无结果」文案也视为渲染完成(真·空结果)
53
+ let rendered = false;
54
+ for (let i = 0; i < %(wait_s)d; i++) {
55
+ await page.waitForTimeout(1000);
56
+ const st = await page.evaluate(() => ({
57
+ arts: document.querySelectorAll('[data-testid="cellInnerDiv"] article').length,
58
+ empty: /没有结果|No results for|无结果/i.test(document.body.innerText || ""),
59
+ }));
60
+ if (st.arts > 0 || st.empty) { rendered = true; break; }
61
+ }
62
+ let items = [];
63
+ if (rendered) {
64
+ for (let s = 0; s < CFG.maxScroll; s++) {
65
+ const cnt = await page.evaluate(() =>
66
+ document.querySelectorAll('[data-testid="cellInnerDiv"] article').length);
67
+ if (cnt >= CFG.n) break;
68
+ await page.evaluate(() => window.scrollBy(0, window.innerHeight * 3));
69
+ await page.waitForTimeout(1200);
70
+ }
71
+ items = await page.evaluate(() => {
72
+ // 互动数从按钮 aria-label 取首个整数(本地化文案不同,但数字恒在前)
73
+ const num = (s) => {
74
+ const m = (s || "").match(/\\d[\\d,\\.]*/);
75
+ return m ? parseInt(m[0].replace(/[,\\.]/g, ""), 10) : 0;
76
+ };
77
+ const out = [];
78
+ const seen = new Set();
79
+ for (const art of document.querySelectorAll('[data-testid="cellInnerDiv"] article')) {
80
+ const t = art.querySelector('[data-testid="tweetText"]');
81
+ const a = art.querySelector('a[href*="/status/"]');
82
+ const tm = art.querySelector('time[datetime]');
83
+ const href = a ? a.href : "";
84
+ if (!href || seen.has(href)) continue;
85
+ seen.add(href);
86
+ const text = t ? t.innerText.trim() : "";
87
+ const like = art.querySelector('[data-testid="like"]');
88
+ const rt = art.querySelector('[data-testid="retweet"]');
89
+ const rp = art.querySelector('[data-testid="reply"]');
90
+ out.push({
91
+ text: text,
92
+ href: href,
93
+ published_at: tm ? tm.getAttribute("datetime") : "",
94
+ likes: num(like && like.getAttribute("aria-label")),
95
+ reposts: num(rt && rt.getAttribute("aria-label")),
96
+ replies: num(rp && rp.getAttribute("aria-label")),
97
+ });
98
+ }
99
+ return out;
100
+ });
101
+ }
102
+ console.log("%(marker)s" + JSON.stringify({ok: rendered, items: items.slice(0, CFG.n)}));
103
+ """
104
+
105
+
106
+ def _build_js(query: str, n: int, sort: str) -> str:
107
+ """把配置注入 JS 模板。"""
108
+ return _JS_TEMPLATE % {
109
+ "cfg": json.dumps({"query": query, "n": n, "sort": sort,
110
+ "maxScroll": MAX_SCROLL}, ensure_ascii=True),
111
+ "base": json.dumps(X_SEARCH_BASE),
112
+ "wait_s": JS_RESULT_WAIT_S,
113
+ "marker": JSON_MARKER,
114
+ }
115
+
116
+
117
+ def _extract_json(stdout: str) -> dict | None:
118
+ """从 ego-browser 输出里取标记行 JSON(CLI 可能夹杂 notice 行)。"""
119
+ for line in reversed((stdout or "").splitlines()):
120
+ if line.startswith(JSON_MARKER):
121
+ try:
122
+ return json.loads(line[len(JSON_MARKER):])
123
+ except json.JSONDecodeError:
124
+ continue
125
+ return None
126
+
127
+
128
+ def _handle_from_url(url: str) -> str:
129
+ """从 status URL 里取作者 handle(https://x.com/<handle>/status/<id>)。"""
130
+ try:
131
+ parts = url.split("/")
132
+ i = parts.index("x.com")
133
+ return parts[i + 1] if i + 1 < len(parts) else ""
134
+ except (ValueError, IndexError):
135
+ return ""
136
+
137
+
138
+ def _rows(items: list[dict], n: int) -> list[dict]:
139
+ """ego-browser 抽取结果 → 统一 result schema(与引擎输出对齐)。"""
140
+ rows: list[dict] = []
141
+ for it in items[:n]:
142
+ href = str(it.get("href") or "").strip()
143
+ if not href.startswith(("http://", "https://")):
144
+ continue
145
+ text = str(it.get("text") or "").strip()
146
+ sid = href.rsplit("/", 1)[-1]
147
+ rows.append({
148
+ "title": (text[:100] + ("..." if len(text) > 100 else "")) or f"Tweet {sid}",
149
+ "url": href,
150
+ "snippet": text[:300],
151
+ "source": "x_search",
152
+ "published_at": it.get("published_at") or None,
153
+ "social_meta": {
154
+ "platform": "twitter",
155
+ "content_type": "tweet",
156
+ "id": sid,
157
+ "author": _handle_from_url(href),
158
+ "likes": it.get("likes") or 0,
159
+ "reposts": it.get("reposts") or 0,
160
+ "replies": it.get("replies") or 0,
161
+ "provider": "x_search",
162
+ },
163
+ })
164
+ return rows
165
+
166
+
167
+ def main(argv: list | None = None) -> int:
168
+ ap = argparse.ArgumentParser(
169
+ prog="argo x-search",
170
+ description="X (Twitter) 关键词搜索(原生布尔语法:from:/min_faves:/-is:retweet/lang:)")
171
+ ap.add_argument("query", help="搜索词,支持 X 原生语法")
172
+ ap.add_argument("-n", "--n", type=int, default=20, help="最多返回条数(默认 20)")
173
+ ap.add_argument("--sort", choices=["latest", "top"], default="latest",
174
+ help="latest=最新(默认,f=live)/ top=热门(f=top)")
175
+ ap.add_argument("--timeout", type=float, default=SUBPROC_TIMEOUT)
176
+ ap.add_argument("--json", action="store_true", help="JSON 输出")
177
+ args = ap.parse_args(argv)
178
+
179
+ query = (args.query or "").strip()
180
+ if not query:
181
+ print("x_search: 搜索词为空", file=sys.stderr)
182
+ return 1
183
+
184
+ if not shutil.which("ego-browser"):
185
+ print("x_search: ego-browser CLI 不存在(Ego Lite 未安装?)", file=sys.stderr)
186
+ return 1
187
+
188
+ sort = "top" if args.sort == "top" else "live"
189
+ try:
190
+ proc = subprocess.run(
191
+ ["ego-browser", "nodejs", "-e", _build_js(query, max(1, args.n), sort)],
192
+ capture_output=True, text=True, encoding="utf-8",
193
+ errors="replace", timeout=args.timeout)
194
+ except subprocess.TimeoutExpired:
195
+ print(f"x_search: ego-browser 超时(>{args.timeout:.0f}s)", file=sys.stderr)
196
+ return 1
197
+ except FileNotFoundError:
198
+ print("x_search: ego-browser 调用失败", file=sys.stderr)
199
+ return 1
200
+
201
+ if proc.returncode != 0:
202
+ tail = (proc.stderr or "").strip()[:200]
203
+ print(f"x_search: ego-browser rc={proc.returncode}: {tail}", file=sys.stderr)
204
+ return 1
205
+
206
+ data = _extract_json(proc.stdout) or _extract_json(proc.stderr)
207
+ if not data:
208
+ print("x_search: 未能解析 ego-browser 输出", file=sys.stderr)
209
+ return 1
210
+ if not data.get("ok"):
211
+ # 页面没渲染:多半是未登录、被风控拦下,或 X 改版
212
+ print("x_search: x.com/search 未渲染(未登录 / 风控 / 改版?)", file=sys.stderr)
213
+ return 1
214
+
215
+ rows = _rows(data.get("items") or [], max(1, args.n))
216
+
217
+ if args.json:
218
+ print(json.dumps(rows, ensure_ascii=False))
219
+ return 0
220
+ if not rows:
221
+ print(f"x_search: 「{query}」无结果", file=sys.stderr)
222
+ return 0
223
+ for i, r in enumerate(rows, 1):
224
+ meta = r.get("social_meta") or {}
225
+ print(f"### {i}. {r['title']}")
226
+ print(f"- **URL**: {r['url']}")
227
+ print(f"- @{meta.get('author', '')} · {r.get('published_at') or ''} · "
228
+ f"♥{meta.get('likes', 0)} ⇄{meta.get('reposts', 0)} 💬{meta.get('replies', 0)}")
229
+ print()
230
+ return 0
231
+
232
+
233
+ if __name__ == "__main__":
234
+ sys.exit(main())
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env python3
2
+ """X (Twitter) Trending 热榜(ego-browser,纯标准库,零文件读写)。
3
+
4
+ 定位:X Trending 热榜是别处拿不到的独家热点源(科技/娱乐/全球趋势分类、
5
+ 带实时排名)。借 ego-browser 驱动用户默认浏览器(Ego Lite)抓取
6
+ x.com/explore/tabs/trending 的渲染结果——复用浏览器 profile 的 cookie 与
7
+ 指纹,避免匿名请求被 X 风控拦下。
8
+
9
+ 与 sogou_weixin_browser 的差别:本脚本**不做任何状态文件读写**(不持久化
10
+ task space、不加文件锁),每次调用自建 task space 抓完即走。取舍:放弃
11
+ 跨调用的会话复用(X 的登录态与 cookie 在浏览器 profile 层,与 task space
12
+ 无关,冷空间实测同样取到数据),换来脚本零路径写入面。
13
+
14
+ 输出:热榜条目(排名、类别、话题、跳转链接),YAML 列表。
15
+
16
+ 依赖:ego-browser CLI(Ego Lite 浏览器)。缺失 / 未运行 / 页面结构变化 →
17
+ stderr 报因、退出码 1(argo _run 对 rc!=0 只取 stderr 留痕,引擎返回空)。
18
+
19
+ 用法:
20
+ python3 scripts/x_trending.py --n 10
21
+ python3 scripts/x_trending.py "热榜" --n 5 # 位置参数兼容引擎契约,忽略
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import shutil
29
+ import subprocess
30
+ import sys
31
+
32
+ JS_RESULT_WAIT_S = 12 # 浏览器内等渲染的墙钟
33
+ SUBPROC_TIMEOUT = 45 # ego-browser 子进程墙钟封顶
34
+ X_TRENDING_URL = "https://x.com/explore/tabs/trending"
35
+ JSON_MARKER = "@@JSON@@"
36
+
37
+ # JS 侧:抓 X Trending 页面。CFG 为 JSON 字面量(ensure_ascii 注入)。
38
+ _JS_TEMPLATE = """
39
+ const CFG = %(cfg)s;
40
+ const task = await taskSpace("argo X热榜");
41
+ let page = null;
42
+ try { page = task.page("p1"); } catch (e) { page = null; }
43
+ if (!page) page = await task.newPage();
44
+ await page.goto(%(url)s);
45
+ // 等热榜渲染(trend 元素出现或超时)
46
+ let got = false;
47
+ for (let i = 0; i < %(wait_s)d; i++) {
48
+ await page.waitForTimeout(1000);
49
+ const n = await page.evaluate(() => document.querySelectorAll('[data-testid="trend"]').length);
50
+ if (n > 0) { got = true; break; }
51
+ }
52
+ let items = [];
53
+ if (got) {
54
+ items = await page.evaluate(() => {
55
+ const out = [];
56
+ for (const el of document.querySelectorAll('[data-testid="trend"]')) {
57
+ const text = el.innerText.trim();
58
+ if (!text) continue;
59
+ // 过滤 Promoted 广告
60
+ if (/promoted by/i.test(text)) continue;
61
+ // X 的行结构:["1", "·", "科技 趋势", "<话题>"],分隔点行要丢掉
62
+ const lines = text.split("\\n").map(s => s.trim())
63
+ .filter(s => s && s !== "·" && s !== ".");
64
+ if (lines.length < 2) continue;
65
+ let rank = "";
66
+ if (/^\\d+$/.test(lines[0])) rank = lines.shift();
67
+ // 话题恒为最后一行;其前一行是分类(趋势类目),再前不再取
68
+ const topic = lines[lines.length - 1].slice(0, 100);
69
+ const category = lines.length >= 2 ? lines[lines.length - 2].slice(0, 40) : "";
70
+ if (!topic) continue;
71
+ const a = el.querySelector("a[href]");
72
+ out.push({ rank, category, topic, href: a ? a.href : "" });
73
+ }
74
+ return out;
75
+ });
76
+ }
77
+ console.log("%(marker)s" + JSON.stringify({
78
+ ok: got,
79
+ items: items.slice(0, CFG.n),
80
+ }));
81
+ """
82
+
83
+
84
+ def _build_js(n: int) -> str:
85
+ """把配置注入 JS 模板。"""
86
+ return _JS_TEMPLATE % {
87
+ "cfg": json.dumps({"n": n}, ensure_ascii=True),
88
+ "url": json.dumps(X_TRENDING_URL),
89
+ "wait_s": JS_RESULT_WAIT_S,
90
+ "marker": JSON_MARKER,
91
+ }
92
+
93
+
94
+ def _extract_json(stdout: str) -> dict | None:
95
+ """从 ego-browser 输出里取标记行 JSON(CLI 可能夹杂 notice 行)。"""
96
+ for line in reversed((stdout or "").splitlines()):
97
+ if line.startswith(JSON_MARKER):
98
+ try:
99
+ return json.loads(line[len(JSON_MARKER):])
100
+ except json.JSONDecodeError:
101
+ continue
102
+ return None
103
+
104
+
105
+ # 泛词:这些 query 只表示「我要看热榜」,不做关键词过滤
106
+ _GENERIC_QUERIES = frozenset({
107
+ "", "热榜", "热搜", "热搜榜", "热门", "热门话题", "趋势", "热议", "今日热搜",
108
+ "trending", "trend", "trends", "hot", "top trends", "hot search",
109
+ })
110
+
111
+
112
+ def _focus(rows: list[dict], query: str) -> list[dict]:
113
+ """位置参数 query 的语义:非泛词时按关键词聚焦热榜。
114
+
115
+ 例:`argo search "AI" --engine x_trending` → 只留命中 AI 的趋势条目。
116
+ 热榜是固定榜单而非检索结果,**无命中时回落全榜**(过滤只是聚焦,
117
+ 不该让「今天 AI 没上榜」表现为引擎空手)。
118
+ """
119
+ q = (query or "").strip()
120
+ if q.casefold() in _GENERIC_QUERIES:
121
+ return rows
122
+ ql = q.casefold()
123
+ hit = [r for r in rows
124
+ if ql in f"{r.get('title', '')} {r.get('snippet', '')}".casefold()]
125
+ return hit or rows
126
+
127
+
128
+ def main(argv: list | None = None) -> int:
129
+ ap = argparse.ArgumentParser(description="X (Twitter) Trending 热榜")
130
+ ap.add_argument("query", nargs="?", default="",
131
+ help="兼容引擎契约的位置参数(本引擎按热榜取数,忽略该值)")
132
+ ap.add_argument("--n", type=int, default=10)
133
+ ap.add_argument("--timeout", type=float, default=SUBPROC_TIMEOUT)
134
+ args = ap.parse_args(argv)
135
+
136
+ if not shutil.which("ego-browser"):
137
+ print("x_trending: ego-browser CLI 不存在(Ego Lite 未安装?)",
138
+ file=sys.stderr)
139
+ return 1
140
+
141
+ try:
142
+ proc = subprocess.run(
143
+ ["ego-browser", "nodejs", "-e", _build_js(args.n)],
144
+ capture_output=True, text=True, encoding="utf-8",
145
+ errors="replace", timeout=args.timeout)
146
+ except subprocess.TimeoutExpired:
147
+ print(f"x_trending: ego-browser 超时(>{args.timeout:.0f}s)", file=sys.stderr)
148
+ return 1
149
+ except FileNotFoundError:
150
+ print("x_trending: ego-browser 调用失败", file=sys.stderr)
151
+ return 1
152
+
153
+ if proc.returncode != 0:
154
+ tail = (proc.stderr or "").strip()[:200]
155
+ print(f"x_trending: ego-browser rc={proc.returncode}: {tail}",
156
+ file=sys.stderr)
157
+ return 1
158
+ data = _extract_json(proc.stdout) or _extract_json(proc.stderr)
159
+ if not data:
160
+ print("x_trending: 未能解析 ego-browser 输出", file=sys.stderr)
161
+ return 1
162
+ if not data.get("ok"):
163
+ print("x_trending: X Trending 页面未渲染或结构变化", file=sys.stderr)
164
+ return 1
165
+
166
+ rows = []
167
+ for it in data.get("items", []):
168
+ topic = str(it.get("topic") or "").strip()
169
+ if not topic:
170
+ continue
171
+ rank = str(it.get("rank") or "").strip()
172
+ category = str(it.get("category") or "").strip()
173
+ href = str(it.get("href") or "").strip()
174
+ if not href.startswith(("http://", "https://")):
175
+ href = f"https://x.com/search?q={topic.replace(' ', '%20')}"
176
+ rows.append({
177
+ "title": (f"{rank}. {topic}" if rank else topic)[:80],
178
+ "url": href,
179
+ "snippet": (f"X Trending | {category}" if category else "X Trending")[:200],
180
+ })
181
+
182
+ import yaml
183
+ rows = _focus(rows, args.query)
184
+ print(yaml.safe_dump(rows[: max(1, args.n)], allow_unicode=True, sort_keys=False))
185
+ return 0
186
+
187
+
188
+ if __name__ == "__main__":
189
+ sys.exit(main())
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """yaml_load.py — YAML 解析的唯一入口(决定用哪个 loader)。
3
+
4
+ ## 为什么需要这个模块
5
+
6
+ PyYAML 有两个语义相同的 SafeLoader 实现:
7
+ - `yaml.SafeLoader` 纯 Python,解析 100 KB 配置实测 **79 ms**
8
+ - `yaml.CSafeLoader` libyaml C 绑定,同样内容实测 **10 ms**(快约 7.7 倍)
9
+
10
+ `yaml.safe_load()` 恒走纯 Python 那条。此前 8 个模块各自直接调它,于是
11
+ 「用哪个解析器」这件事被复制了 8 份,没有任何一处能统一改进——与 fetch
12
+ 的 `--focus` 参数同一种失败模式(各写一份 → 漏改无人察觉)。
13
+
14
+ 本模块是 2026-09-15 冷启动优化的一部分:`import search` 曾耗时约 500 ms,
15
+ 其中约 316 ms 是 4 次 `peek_cache_db_path()` 各自解析一遍 123 KB 的
16
+ config.yaml,而每次解析的绝大部分时间花在纯 Python 扫描器上。换成 C 版
17
+ 后单次降到约 10 ms。
18
+
19
+ ## 用法
20
+
21
+ from yaml_load import loads, load
22
+
23
+ data = loads(text) # 解析字符串
24
+ data = load(path) # 读取并解析文件
25
+
26
+ 两者在 PyYAML 缺失时抛 ImportError(与 config._require_yaml 的原契约一致,
27
+ 调用方按既有 fail-open 语义自行处理),解析失败抛 yaml.YAMLError。
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ __all__ = ["loads", "load", "safe_loader"]
36
+
37
+
38
+ def _yaml():
39
+ import yaml # type: ignore
40
+ return yaml
41
+
42
+
43
+ def safe_loader() -> Any:
44
+ """返回可用的安全 loader 类:优先 libyaml C 版,不可用回落纯 Python。
45
+
46
+ 两者语义一致(C 版就是 SafeLoader 的 C 实现),差别只在速度。
47
+ 纯 Python 环境(未编译 libyaml)下行为与改动前完全相同。
48
+ """
49
+ yaml = _yaml()
50
+ return getattr(yaml, "CSafeLoader", None) or yaml.SafeLoader
51
+
52
+
53
+ def loads(text: str) -> Any:
54
+ """解析 YAML 文本(安全 loader)。非 dict/list 的顶层结构按原样返回。"""
55
+ yaml = _yaml()
56
+ return yaml.load(text, Loader=safe_loader())
57
+
58
+
59
+ def load(path: str | Path) -> Any:
60
+ """读取并解析 YAML 文件(UTF-8)。"""
61
+ text = Path(path).read_text(encoding="utf-8")
62
+ try:
63
+ return loads(text)
64
+ except Exception:
65
+ # 与 yaml.safe_load 对 file 对象的错误语义保持一致:把非 YAML 内容
66
+ # 造成的问题留给调用方判断(此前各自直接 safe_load 也是这个行为)
67
+ raise