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
@@ -0,0 +1,638 @@
1
+ #!/usr/bin/env python3
2
+ """engine_validate.py — 引擎标准化验证(准入检查)
3
+
4
+ 用法:
5
+ python3 scripts/engine_validate.py --engine hackernews --stage health
6
+ python3 scripts/engine_validate.py --engine hackernews --stage quality
7
+ python3 scripts/engine_validate.py --engine hackernews --stage all --admit
8
+ python3 scripts/engine_validate.py --all-free --stage health
9
+ python3 scripts/engine_validate.py --engine my_new --stage health --admit
10
+
11
+ Stage:
12
+ health — 连通性 + 结果 schema + 延迟
13
+ quality — 固定 query 集(空结果率 / 字段完整率 / 延迟)
14
+ all — health + quality
15
+
16
+ 通过 health 且 --admit 时写入 admission(blocked=false)。
17
+ 缺 Key:status=skipped,不算失败,不写 block(除非 --block-on-skip)。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ SCRIPT_DIR = Path(__file__).resolve().parent
29
+ ROOT = SCRIPT_DIR.parent
30
+ if str(SCRIPT_DIR) not in sys.path:
31
+ sys.path.insert(0, str(SCRIPT_DIR))
32
+
33
+ from config import load_config # noqa: E402
34
+ from engine_env import missing_env_for, env_ready # noqa: E402
35
+ from engine_admission import record_validation, load_admission # noqa: E402
36
+ from engine_status import engine_detail # noqa: E402
37
+ from cli_io import dumps
38
+
39
+ REQUIRED_KEYS = ("title", "url", "source")
40
+
41
+ # 垂类引擎 health canary(通用词会误杀快讯/行情类)
42
+ ENGINE_CANARY_QUERIES: dict[str, str] = {
43
+ "fxtwitter": "OpenAI",
44
+ "twitter": "OpenAI",
45
+ "jin10": "美联储",
46
+ "cls_telegraph": "股市",
47
+ "em_global_news": "股市",
48
+ "ths_hot": "热点",
49
+ "eastmoney": "贵州茅台",
50
+ "qweather": "北京 天气",
51
+ "finviz": "AAPL",
52
+ "seeking_alpha": "AAPL",
53
+ "arxiv": "transformer",
54
+ "semantic_scholar": "attention mechanism",
55
+ "google_scholar": "machine learning",
56
+ "hackernews": "Python",
57
+ "stackoverflow": "python asyncio",
58
+ "v2ex": "Python",
59
+ "wechat_sogou": "人工智能",
60
+ }
61
+
62
+ # 质量基准:少而稳;默认偏通用/技术,避免垂类引擎被无关 query 误杀
63
+ # 金融/中文垂类可在调用时传入自定义 queries
64
+ QUALITY_QUERIES: list[dict[str, str]] = [
65
+ {"id": "tech_en", "query": "Python asyncio", "category": "tech"},
66
+ {"id": "tech_lib", "query": "open source LLM", "category": "tech"},
67
+ {"id": "news_en", "query": "OpenAI research", "category": "news"},
68
+ {"id": "general", "query": "machine learning", "category": "general"},
69
+ {"id": "dev", "query": "kubernetes networking", "category": "tech"},
70
+ ]
71
+
72
+ # 中文/金融场景补充集(--profile cn 时使用)
73
+ QUALITY_QUERIES_CN: list[dict[str, str]] = [
74
+ {"id": "tech_zh", "query": "Python 异步编程", "category": "tech_zh"},
75
+ {"id": "finance_zh", "query": "沪深300 指数", "category": "finance"},
76
+ {"id": "general_zh", "query": "人工智能 应用", "category": "general"},
77
+ {"id": "news_zh", "query": "美联储 利率", "category": "news"},
78
+ {"id": "market_zh", "query": "ETF 增强策略", "category": "finance"},
79
+ ]
80
+
81
+
82
+ def _get_spec(engine_id: str) -> dict[str, Any]:
83
+ cfg = load_config(force=True)
84
+ return dict((cfg.get("engines") or {}).get(engine_id) or {})
85
+
86
+
87
+ def _schema_ok(results: list[dict[str, Any]]) -> tuple[bool, str, float]:
88
+ """返回 (ok, message, field_complete_rate)。"""
89
+ if not isinstance(results, list):
90
+ return False, "results 非 list", 0.0
91
+ if not results:
92
+ return False, "空结果", 0.0
93
+ complete = 0
94
+ total = 0
95
+ for i, r in enumerate(results):
96
+ if not isinstance(r, dict):
97
+ return False, f"results[{i}] 非 dict", 0.0
98
+ if "error" in r and not r.get("title"):
99
+ continue
100
+ total += 1
101
+ missing = [k for k in REQUIRED_KEYS if not r.get(k)]
102
+ # title 或 url 至少一个;source 应用 _engine 回填,可缺
103
+ if r.get("title") or r.get("url"):
104
+ if not missing or missing == ["source"]:
105
+ complete += 1
106
+ elif "title" not in missing or "url" not in missing:
107
+ # 有 title 或 url 算半完整
108
+ complete += 0.5
109
+ else:
110
+ return False, f"results[{i}] 缺 title/url", complete / max(total, 1)
111
+ if total == 0:
112
+ return False, "无有效条目", 0.0
113
+ rate = complete / total
114
+ ok = rate >= 0.5
115
+ return ok, f"字段完整率 {rate:.0%}", rate
116
+
117
+
118
+ # 查询语言 → 该引擎需声明的 coverage 标签;声明 general 视为不限语言。
119
+ _RELEVANCE_LANG_TAGS: dict[str, tuple[str, ...]] = {
120
+ "zh": ("chinese", "zh"),
121
+ "en": ("english", "en"),
122
+ "ja": ("japanese", "ja"),
123
+ "ko": ("korean", "ko"),
124
+ }
125
+
126
+
127
+ def _relevance_applicable(spec: dict[str, Any], query: str) -> tuple[bool, str]:
128
+ """相关性判据是否适用于本次探针。
129
+
130
+ 不适用(返回 False)的三种情形,都是为避免误杀而非放水:
131
+ 1. 引擎声明 `relevance_check: false`:热榜/快讯/列表型源按设计
132
+ 忽略查询词,拿查询词去评判它们等于用错标准。标准不降,只是
133
+ 对这类源问错了问题。
134
+ 2. 查询为空或取不到词元。
135
+ 3. 查询语言不在该引擎声明的 coverage 内:词面比较在跨语言下
136
+ 没有意义(中文查询配纯英文源,零字符交集不代表结果不相关)。
137
+ """
138
+ if spec.get("relevance_check") is False:
139
+ return False, "declared_off"
140
+ if not query or not query.strip():
141
+ return False, "empty_query"
142
+ try:
143
+ from tfidf_router import tokenize
144
+ if not set(tokenize(query)):
145
+ return False, "no_tokens"
146
+ except ImportError:
147
+ return False, "tokenizer_unavailable"
148
+ try:
149
+ from lang_detect import detect_language, is_non_latin_lang
150
+ lang = detect_language(query)
151
+ except Exception:
152
+ return True, ""
153
+ cov = set(spec.get("coverage") or [])
154
+ if not cov or "general" in cov:
155
+ return True, ""
156
+ # 跨语言守卫**只对非拉丁查询生效**。中文查询配纯英文源时词面零交集不
157
+ # 代表结果不相关,判据没意义;反过来英文查询几乎所有源都能处理,若
158
+ # 要求 coverage 必须声明 english,会把只声明内容域的源(mdn 的 docs、
159
+ # github 的 code、arxiv 的 academic)全部误跳过,判据就形同虚设了。
160
+ want = _RELEVANCE_LANG_TAGS.get(lang)
161
+ if want and is_non_latin_lang(lang) and not (cov & set(want)):
162
+ return False, f"cross_lang_{lang}_engine_no_{lang}"
163
+ return True, ""
164
+
165
+
166
+ def _relevance_check(spec: dict[str, Any], results: list[dict[str, Any]],
167
+ query: str) -> dict[str, Any]:
168
+ """结果与查询有无词面交集。
169
+
170
+ 为什么要这道判据:schema 完整率只看字段齐不齐。检索层被接错语料的引擎
171
+ (2026-09-16 Seltz 实测:中文查询落 news 语料,返回时政与播客条目)字段
172
+ 100% 完整、延迟正常,在旧判据下拿满分 1.0 通过准入。结构合法但答非所问,
173
+ 只能靠相关性判据挡。
174
+
175
+ 判负门槛取**零交集**而非「多数命中」。实测依据:mdn 的 canary 是单词
176
+ "Python",它工作正常却只有 0.2 命中率(MDN 的 Python 章节页标题未必都
177
+ 含 python 字样)——用多数规则会误杀好引擎。零交集没有这种歧义,也是
178
+ 「答非所问」唯一无争议的形态。命中率照常上报,供人判断灰度。
179
+
180
+ 返回 ok=True 表示「不适用或通过」。不适用时 hit_rate 为 None,调用方
181
+ 不得把它算作失败。
182
+ """
183
+ applicable, skip_reason = _relevance_applicable(spec, query)
184
+ if not applicable:
185
+ return {"applicable": False, "ok": True, "hit_rate": None,
186
+ "relevance_msg": f"not_applicable: {skip_reason}"}
187
+ if not results:
188
+ # 空结果由 schema 判据负责("空结果"),此处不重复记账,
189
+ # 否则 error 归因会被相关性抢走,看不出真实原因。
190
+ return {"applicable": False, "ok": True, "hit_rate": None,
191
+ "relevance_msg": "not_applicable: no_results"}
192
+ try:
193
+ from tfidf_router import tokenize
194
+ q_set = set(tokenize(query))
195
+ covered = sum(
196
+ 1 for r in results
197
+ if q_set & set(tokenize(f"{r.get('title') or ''} {r.get('snippet') or ''}"))
198
+ )
199
+ hit_rate = round(covered / len(results), 3)
200
+ except Exception as e:
201
+ # 判据不可用时不设卡(fail-open),但如实记明原因,避免静默通过
202
+ return {"applicable": False, "ok": True, "hit_rate": None,
203
+ "relevance_msg": f"not_applicable: check_unavailable({type(e).__name__})"}
204
+ return {"applicable": True, "ok": hit_rate > 0.0, "hit_rate": hit_rate,
205
+ "relevance_msg": "" if hit_rate > 0.0 else "结果与查询零词面交集(疑似语料选错)"}
206
+
207
+
208
+ def run_health(engine_id: str, *, query: str | None = None, n: int = 3,
209
+ timeout: float = 10.0) -> dict[str, Any]:
210
+ """连通性 + schema 健康检查。"""
211
+ from engines import search as engine_search, get_registry
212
+
213
+ spec = _get_spec(engine_id)
214
+ if not spec:
215
+ return {
216
+ "ok": False,
217
+ "status": "error",
218
+ "engine": engine_id,
219
+ "error": "engine_not_in_config",
220
+ }
221
+ if not spec.get("enabled", True):
222
+ return {
223
+ "ok": False,
224
+ "status": "skipped",
225
+ "engine": engine_id,
226
+ "error": "disabled_in_config",
227
+ }
228
+ if not env_ready(engine_id, spec):
229
+ return {
230
+ "ok": False,
231
+ "status": "skipped",
232
+ "engine": engine_id,
233
+ "error": "missing_env",
234
+ "missing_env": missing_env_for(engine_id, spec),
235
+ }
236
+
237
+ # 确保已加载
238
+ reg = get_registry()
239
+ if engine_id not in reg:
240
+ return {
241
+ "ok": False,
242
+ "status": "error",
243
+ "engine": engine_id,
244
+ "error": "no_builder_or_not_registered",
245
+ "type": spec.get("type"),
246
+ }
247
+
248
+ # 垂类 canary:spec.canary_query > 内置表 > 默认 Python
249
+ if not query:
250
+ query = (
251
+ spec.get("canary_query")
252
+ or ENGINE_CANARY_QUERIES.get(engine_id)
253
+ or "Python"
254
+ )
255
+
256
+ t0 = time.time()
257
+ try:
258
+ results = engine_search(query, engine_id, n=n, timeout=timeout)
259
+ except Exception as e:
260
+ return {
261
+ "ok": False,
262
+ "status": "error",
263
+ "engine": engine_id,
264
+ "error": f"{type(e).__name__}: {e}",
265
+ "latency_ms": round((time.time() - t0) * 1000, 1),
266
+ }
267
+ latency = round((time.time() - t0) * 1000, 1)
268
+ results = results if isinstance(results, list) else []
269
+ schema_ok, schema_msg, field_rate = _schema_ok(results)
270
+
271
+ # 回填 source 再评一次(部分引擎只靠 _engine)
272
+ if not schema_ok and results:
273
+ patched = []
274
+ for r in results:
275
+ if isinstance(r, dict):
276
+ rr = dict(r)
277
+ rr.setdefault("source", engine_id)
278
+ patched.append(rr)
279
+ schema_ok, schema_msg, field_rate = _schema_ok(patched)
280
+
281
+ ok = schema_ok and latency <= timeout * 1000 * 1.2
282
+ rel = _relevance_check(spec, results, query)
283
+ if not rel["ok"]:
284
+ ok = False
285
+ return {
286
+ "ok": ok,
287
+ "status": "pass" if ok else "fail",
288
+ "engine": engine_id,
289
+ "query": query,
290
+ "count": len(results),
291
+ "latency_ms": latency,
292
+ "schema_ok": schema_ok,
293
+ "schema_msg": schema_msg,
294
+ "field_complete_rate": round(field_rate, 3),
295
+ "sample_title": (results[0].get("title") if results and isinstance(results[0], dict) else None),
296
+ "relevance_ok": rel["ok"],
297
+ "relevance_applicable": rel["applicable"],
298
+ "relevance_hit_rate": rel["hit_rate"],
299
+ "relevance_msg": rel["relevance_msg"],
300
+ # 失败原因按「先归因再报错」排序:schema 问题 > 相关性 > 超时
301
+ "error": None if ok else (
302
+ schema_msg if not schema_ok
303
+ else (rel["relevance_msg"] or "relevance_failed") if not rel["ok"]
304
+ else "timeout_or_fail"
305
+ ),
306
+ }
307
+
308
+
309
+ def run_quality(engine_id: str, *, queries: list[dict[str, str]] | None = None,
310
+ n: int = 3, timeout: float = 10.0,
311
+ max_queries: int = 5,
312
+ profile: str = "default") -> dict[str, Any]:
313
+ """质量基准:多 query 空结果率、平均延迟、字段完整率。"""
314
+ spec = _get_spec(engine_id)
315
+ if not spec or not spec.get("enabled", True):
316
+ return {"ok": False, "status": "skipped", "engine": engine_id, "error": "disabled_or_missing"}
317
+ if not env_ready(engine_id, spec):
318
+ return {
319
+ "ok": False,
320
+ "status": "skipped",
321
+ "engine": engine_id,
322
+ "error": "missing_env",
323
+ "missing_env": missing_env_for(engine_id, spec),
324
+ }
325
+
326
+ if queries is not None:
327
+ base_qs = queries
328
+ else:
329
+ # 引擎自述的适用查询集优先:候选池型引擎(如 v2ex 只能取官方 API 的
330
+ # 热帖/最新帖再本地过滤)对通用技术查询天然空结果,用通用集评估会把
331
+ # 「检索模式不匹配」误判成「引擎质量差」。让引擎声明自己的适用集,
332
+ # 比放宽阈值诚实——标准不降,只是问对问题。
333
+ declared = spec.get("quality_queries")
334
+ declared_qs: list[dict[str, str]] = []
335
+ if isinstance(declared, list):
336
+ declared_qs = [
337
+ {"id": q.get("id") or f"declared_{i}",
338
+ "query": q["query"],
339
+ "category": q.get("category") or "declared"}
340
+ for i, q in enumerate(declared)
341
+ if isinstance(q, dict) and q.get("query")
342
+ ]
343
+ if declared_qs:
344
+ base_qs = declared_qs
345
+ elif profile == "cn":
346
+ base_qs = QUALITY_QUERIES_CN
347
+ else:
348
+ base_qs = QUALITY_QUERIES
349
+ qs = base_qs[:max_queries]
350
+ runs: list[dict[str, Any]] = []
351
+ for item in qs:
352
+ h = run_health(engine_id, query=item["query"], n=n, timeout=timeout)
353
+ runs.append({
354
+ "id": item.get("id"),
355
+ "query": item["query"],
356
+ "category": item.get("category"),
357
+ "ok": h.get("ok"),
358
+ "count": h.get("count", 0),
359
+ "latency_ms": h.get("latency_ms"),
360
+ "field_complete_rate": h.get("field_complete_rate", 0),
361
+ "relevance_applicable": h.get("relevance_applicable"),
362
+ "relevance_hit_rate": h.get("relevance_hit_rate"),
363
+ "error": h.get("error"),
364
+ })
365
+
366
+ total = len(runs) or 1
367
+ empty = sum(1 for r in runs if not r.get("count"))
368
+ empty_rate = empty / total
369
+ latencies = [r["latency_ms"] for r in runs if isinstance(r.get("latency_ms"), (int, float))]
370
+ avg_lat = sum(latencies) / len(latencies) if latencies else None
371
+ field_rates = [r.get("field_complete_rate") or 0 for r in runs]
372
+ avg_field = sum(field_rates) / len(field_rates) if field_rates else 0.0
373
+ pass_n = sum(1 for r in runs if r.get("ok"))
374
+ pass_rate = pass_n / total
375
+
376
+ # 相关性:只统计判据适用的那些 query(不适用的不算失败,也不算通过)
377
+ rel_runs = [r for r in runs if r.get("relevance_applicable")]
378
+ # hit_rate 为 None 时 None == 0.0 即 False,无需额外判空
379
+ rel_fail = sum(1 for r in rel_runs if r.get("relevance_hit_rate") == 0.0)
380
+ rel_applicable_n = len(rel_runs)
381
+ rel_fail_rate = (rel_fail / rel_applicable_n) if rel_applicable_n else 0.0
382
+
383
+ # 质量分:通过率 0.5 + (1-空结果率)*0.3 + 字段完整 0.2
384
+ quality_score = round(pass_rate * 0.5 + (1 - empty_rate) * 0.3 + avg_field * 0.2, 3)
385
+ # 相关性多数不通过时给分封顶:字段齐全、计数正常但答非所问的引擎
386
+ # 不该继续拿高分(Seltz 2026-09-16 就是这么拿到 1.0 的)。
387
+ relevance_capped = False
388
+ if rel_applicable_n and rel_fail_rate > 0.5:
389
+ quality_score = min(quality_score, 0.3)
390
+ relevance_capped = True
391
+ ok = pass_rate >= 0.4 and empty_rate <= 0.6 and rel_fail_rate <= 0.5
392
+
393
+ return {
394
+ "ok": ok,
395
+ "status": "pass" if ok else "fail",
396
+ "engine": engine_id,
397
+ "quality_score": quality_score,
398
+ "pass_rate": round(pass_rate, 3),
399
+ "empty_rate": round(empty_rate, 3),
400
+ "relevance_applicable": rel_applicable_n,
401
+ "relevance_fail": rel_fail,
402
+ "relevance_fail_rate": round(rel_fail_rate, 3),
403
+ "relevance_capped": relevance_capped,
404
+ "avg_latency_ms": round(avg_lat, 1) if avg_lat is not None else None,
405
+ "avg_field_complete_rate": round(avg_field, 3),
406
+ "runs": runs,
407
+ }
408
+
409
+
410
+ def validate_engine(
411
+ engine_id: str,
412
+ *,
413
+ stage: str = "health",
414
+ admit: bool = False,
415
+ timeout: float = 10.0,
416
+ write_doc: bool = False,
417
+ profile: str = "default",
418
+ ) -> dict[str, Any]:
419
+ """执行验证并可选写入 admission。"""
420
+ stages = []
421
+ if stage == "all":
422
+ stages = ["health", "quality"]
423
+ elif stage in ("health", "quality"):
424
+ stages = [stage]
425
+ else:
426
+ return {"ok": False, "error": f"unknown stage: {stage}"}
427
+
428
+ report: dict[str, Any] = {
429
+ "engine": engine_id,
430
+ "stage_requested": stage,
431
+ "stages": {},
432
+ "ok": True,
433
+ "status": "pass",
434
+ "profile": profile,
435
+ }
436
+ passed: list[str] = []
437
+ health_res = None
438
+ quality_res = None
439
+
440
+ if "health" in stages:
441
+ health_res = run_health(engine_id, timeout=timeout)
442
+ report["stages"]["health"] = health_res
443
+ if health_res.get("status") == "skipped":
444
+ report["ok"] = False
445
+ report["status"] = "skipped"
446
+ report["skip_reason"] = health_res.get("error")
447
+ elif health_res.get("ok"):
448
+ passed.append("health")
449
+ else:
450
+ report["ok"] = False
451
+ report["status"] = "fail"
452
+
453
+ if "quality" in stages and report.get("status") != "skipped":
454
+ quality_res = run_quality(engine_id, timeout=timeout, profile=profile)
455
+ report["stages"]["quality"] = quality_res
456
+ if quality_res.get("status") == "skipped":
457
+ report["ok"] = False
458
+ report["status"] = "skipped"
459
+ report["skip_reason"] = quality_res.get("error")
460
+ elif quality_res.get("ok"):
461
+ passed.append("quality")
462
+ else:
463
+ report["ok"] = False
464
+ if report["status"] != "skipped":
465
+ report["status"] = "fail"
466
+
467
+ # 写 admission
468
+ if report.get("status") != "skipped":
469
+ blocked = not ("health" in passed)
470
+ # reason 只在**被 block** 时写:它表达「为什么不能用」。
471
+ # 未 block 时留空——`record_validation` 的不变式是
472
+ # 「blocked=False ⇒ reason 为空」,成功路径上写
473
+ # `validation_passed` 会直接违反该不变式(实测线上 64 条记录
474
+ # blocked=false/reason=validation_passed,与注释宣称的语义矛盾)。
475
+ # 成功与否由 blocked/admitted_at/stages_passed 表达,不需要 reason。
476
+ if blocked:
477
+ reason = (health_res or {}).get("error") or "health_failed"
478
+ else:
479
+ reason = ""
480
+ avg_lat = None
481
+ qscore = None
482
+ if quality_res:
483
+ avg_lat = quality_res.get("avg_latency_ms")
484
+ qscore = quality_res.get("quality_score")
485
+ elif health_res:
486
+ avg_lat = health_res.get("latency_ms")
487
+ adm = record_validation(
488
+ engine_id,
489
+ stages_passed=passed,
490
+ quality_score=qscore,
491
+ avg_latency_ms=avg_lat,
492
+ blocked=blocked if ("health" in stages) else None,
493
+ reason=reason,
494
+ health=health_res,
495
+ quality=quality_res,
496
+ admit=admit and ("health" in passed),
497
+ )
498
+ report["admission"] = adm
499
+ else:
500
+ report["admission"] = load_admission(engine_id)
501
+
502
+ if write_doc and report.get("status") == "pass":
503
+ path = _write_engine_doc(engine_id, report)
504
+ report["doc_path"] = str(path)
505
+
506
+ report["detail"] = engine_detail(engine_id)
507
+ return report
508
+
509
+
510
+ def _write_engine_doc(engine_id: str, report: dict[str, Any]) -> Path:
511
+ """生成 docs/engines/<id>.md 运行手册。"""
512
+ docs = ROOT / "docs" / "engines"
513
+ docs.mkdir(parents=True, exist_ok=True)
514
+ path = docs / f"{engine_id}.md"
515
+ detail = report.get("detail") or engine_detail(engine_id)
516
+ adm = report.get("admission") or {}
517
+ health = (report.get("stages") or {}).get("health") or {}
518
+ quality = (report.get("stages") or {}).get("quality") or {}
519
+ lines = [
520
+ f"# 引擎:`{engine_id}`",
521
+ "",
522
+ f"- 准入时间: {adm.get('admitted_at') or '(未标记 admit)'}",
523
+ f"- 状态: {detail.get('status')}",
524
+ f"- cost_tier: {detail.get('cost_tier')}",
525
+ f"- type: {detail.get('type')}",
526
+ f"- quality_score: {adm.get('quality_score')}",
527
+ f"- avg_latency_ms: {adm.get('avg_latency_ms')}",
528
+ f"- blocked: {adm.get('blocked')}",
529
+ "",
530
+ "## 环境变量",
531
+ "",
532
+ "| 变量 | 必填 |",
533
+ "|------|------|",
534
+ ]
535
+ req = detail.get("required_env") or []
536
+ if not req:
537
+ lines.append("| (无) | 否 |")
538
+ else:
539
+ for v in req:
540
+ lines.append(f"| `{v}` | 是 |")
541
+ lines += [
542
+ "",
543
+ "## 最近验证",
544
+ "",
545
+ f"- health: {health.get('status')} · latency={health.get('latency_ms')}ms · count={health.get('count')}",
546
+ f"- quality: {quality.get('status')} · score={quality.get('quality_score')} · empty_rate={quality.get('empty_rate')}",
547
+ "",
548
+ "## 调用",
549
+ "",
550
+ "```bash",
551
+ f'python3 scripts/search.py "查询词" --engine {engine_id}',
552
+ f"python3 scripts/engine_validate.py --engine {engine_id} --stage health",
553
+ "```",
554
+ "",
555
+ ]
556
+ path.write_text("\n".join(lines), encoding="utf-8")
557
+ return path
558
+
559
+
560
+ def _free_engine_ids() -> list[str]:
561
+ from config import get_cost_tiers
562
+ cfg = load_config()
563
+ tiers = get_cost_tiers(cfg)
564
+ free = set(tiers.get("free") or [])
565
+ out = []
566
+ for name, spec in (cfg.get("engines") or {}).items():
567
+ if not isinstance(spec, dict) or not spec.get("enabled", True):
568
+ continue
569
+ if name in free or (name not in (tiers.get("paid") or []) and name not in (tiers.get("low") or []) and name not in (tiers.get("api") or [])):
570
+ # 跳过需要 key 的
571
+ if env_ready(name, spec):
572
+ out.append(name)
573
+ return sorted(out)
574
+
575
+
576
+ def main() -> int:
577
+ parser = argparse.ArgumentParser(description="Argo 引擎标准化验证")
578
+ parser.add_argument("--engine", "-e", help="引擎 ID")
579
+ parser.add_argument("--stage", default="health", choices=["health", "quality", "all"])
580
+ parser.add_argument("--admit", action="store_true", help="通过后写入准入状态")
581
+ parser.add_argument("--write-doc", action="store_true", help="通过后生成 docs/engines/<id>.md")
582
+ parser.add_argument("--timeout", type=float, default=10.0)
583
+ parser.add_argument("--profile", default="default", choices=["default", "cn"],
584
+ help="quality 查询集:default=通用/技术,cn=中文/金融")
585
+ parser.add_argument("--json", action="store_true")
586
+ parser.add_argument("--all-free", action="store_true", help="对所有 free 且 env 就绪的引擎跑 health")
587
+ parser.add_argument("--engines", help="逗号分隔多个引擎")
588
+ args = parser.parse_args()
589
+
590
+ targets: list[str] = []
591
+ if args.all_free:
592
+ targets = _free_engine_ids()
593
+ elif args.engines:
594
+ targets = [x.strip() for x in args.engines.split(",") if x.strip()]
595
+ elif args.engine:
596
+ targets = [args.engine]
597
+ else:
598
+ parser.error("请指定 --engine / --engines / --all-free")
599
+
600
+ reports = []
601
+ exit_ok = True
602
+ for eng in targets:
603
+ # all-free 默认只跑 health 且不强制 admit 全部
604
+ stage = args.stage
605
+ admit = args.admit
606
+ if args.all_free and args.stage == "all" and len(targets) > 3:
607
+ stage = "health"
608
+ rep = validate_engine(
609
+ eng, stage=stage, admit=admit, timeout=args.timeout,
610
+ write_doc=args.write_doc, profile=args.profile,
611
+ )
612
+ reports.append(rep)
613
+ if rep.get("status") == "fail":
614
+ exit_ok = False
615
+ if not args.json:
616
+ st = rep.get("status")
617
+ lat = ""
618
+ h = (rep.get("stages") or {}).get("health") or {}
619
+ if h.get("latency_ms") is not None:
620
+ lat = f" {h['latency_ms']}ms"
621
+ q = (rep.get("stages") or {}).get("quality") or {}
622
+ qnote = f" q={q.get('quality_score')}" if q else ""
623
+ print(f"[{st}] {eng}{lat}{qnote} stages_passed={ (rep.get('admission') or {}).get('stages_passed')}")
624
+ if rep.get("skip_reason"):
625
+ print(f" skip: {rep['skip_reason']}")
626
+ if st == "fail":
627
+ err = h.get("error") or q.get("error") or rep.get("error")
628
+ print(f" error: {err}")
629
+
630
+ if args.json:
631
+ payload = reports if len(reports) > 1 else reports[0]
632
+ print(dumps(payload))
633
+
634
+ return 0 if exit_ok else 1
635
+
636
+
637
+ if __name__ == "__main__":
638
+ raise SystemExit(main())