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,732 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ engine_families.py — 搜索源能力族分类(第一性原理重构)
4
+
5
+ 问题重定义:
6
+ 旧:每个搜索源是独立个体,type/cost/coverage 散落,路由靠 domain 手写 combo,
7
+ 同类源(如 byted/bocha 都是全网搜索)无法互换、无法统一测试。
8
+ 新:搜索源按「检索能力」归族(MECE),同族源共享统一调用契约——
9
+ 任意组合、标准化输入输出、金标集按族检查、A/B 可替换。
10
+
11
+ 能力族(MECE,互斥且穷尽):
12
+ web_general 全网网页检索(多语言) byted / bocha / tavily / anysearch / octen / exa
13
+ web_chinese 中文全网检索 bocha / byted / wechat_sogou / local_bing 等
14
+ academic 学术文献 arxiv / openalex / crossref / semantic_scholar / dblp / europepmc
15
+ code 代码/包/文档 github / pypi / npm / mdn / stackoverflow / crates / gitlab / devto
16
+ finance_market 行情/资金 sina_quote / tencent_quote / em_flow / finviz
17
+ finance_macro 宏观数据 fred / worldbank / nbs_stats / eurostat / fx_rate
18
+ news_flash 快讯/电报 cls_telegraph / em_global_news / jin10 / em_miaoxiang
19
+ social 社区 UGC zhihu / zhihu_global / v2ex / juejin / reddit / twitter / xiaohongshu / bilibili / weibo
20
+ personal_data 个人数据(本人创作/收藏/关注) zhihu_user
21
+ hot_trending 热榜 baidu_hot / toutiao_hot / bilibili_hot / zhihu_hot / ths_hot
22
+ knowledge 百科/实体 wikipedia / baidu_baike / zh_wikipedia / wikidata / moegirl / free_dictionary
23
+ science_chem 化学/药学 pubchem / openfda / clinicaltrials
24
+ science_bio 生物/蛋白 uniprot / rcsb_pdb / gbif
25
+ science_geo 地球/空间 usgs / nasa_cmr
26
+ legal 法律判例 courtlistener / wenshu
27
+ media_book 媒体/图书 itunes / open_library / weread / douban_book / gutenberg / musicbrainz / openverse / imdb
28
+ sports 体育赛事/球员球队 thesportsdb
29
+ archive 归档/历史 wayback_cdx / archive_org
30
+ misc_vertical 其他垂直(游戏/预测/天气/标准等) steam / polymarket / qweather / rfc_editor / models_dev / itotii / urban_dictionary / know_your_meme / coingecko / docker_hub / huggingface / stackoverflow 等
31
+ structured_card 垂直结构化模态卡(统一语义识别,跨域) bocha_ai
32
+
33
+ 用途:
34
+ - route 层组合时按 family 去重(同族至多 N 个,避免同质源堆叠)
35
+ - 去重腾出的槽位用互补能力族引擎回填(complement_refill)
36
+ - 金标集按族检查而非按引擎名(源可替换不破坏测试)
37
+ - engine_status / list-engines 展示按族分组
38
+ - research profile 的 vertical_engines 可按 family 扩展
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from typing import Any
44
+
45
+ # ── 引擎 → 能力族 映射(config.yaml 引擎声明的 family 字段优先,本表保底) ──
46
+
47
+ # 默认族:未声明 family 的引擎归 web_general(全网搜索是通用保底)
48
+ DEFAULT_FAMILY = "web_general"
49
+
50
+ # 引擎名 → family(显式覆盖表;config.yaml 声明 family 时以其为准)
51
+ _ENGINE_FAMILY_OVERRIDES: dict[str, str] = {
52
+ # 全网搜索
53
+ "byted": "web_general",
54
+ "bocha": "web_general",
55
+ "tavily": "web_general",
56
+ "anysearch": "web_general",
57
+ "octen": "web_general",
58
+ "exa": "web_general",
59
+ "uapi": "web_general",
60
+ "searxng": "web_general",
61
+ # zhihu_global 是真全网搜索(Filter host== 可搜非知乎站),非社区站内
62
+ "zhihu_global": "web_general",
63
+ # 中文全网(子集,语义上仍是 web_general,但标记中文能力)
64
+ "wechat_sogou": "web_general",
65
+ "local_bing": "web_general",
66
+ "local_baidu": "web_general",
67
+ "local_sogou": "web_general",
68
+ "local_360": "web_general",
69
+ "local_jisilu": "social",
70
+ # 全网搜索引擎四兄弟(ecosia/qwant/shenma/so 显式_only 或中文移动端,
71
+ # 能力上与 local_baidu/local_bing 同族)
72
+ "ecosia": "web_general",
73
+ "qwant": "web_general",
74
+ "shenma": "web_general",
75
+ "so": "web_general",
76
+ # 批次十(2026-09-26):bing_rss 是 local_bing 的 RSS 稳定出口;
77
+ # tineye 反搜图不属于任何既有能力族,归 misc_vertical。
78
+ "bing_rss": "web_general",
79
+ "tineye": "misc_vertical",
80
+ # 学术
81
+ "arxiv": "academic",
82
+ "openalex": "academic",
83
+ "crossref": "academic",
84
+ "semantic_scholar": "academic",
85
+ "dblp": "academic",
86
+ "europepmc": "academic",
87
+ "google_scholar": "academic",
88
+ "local_arxiv": "academic",
89
+ "local_crossref": "academic",
90
+ "local_semantic_scholar": "academic",
91
+ "local_pubmed": "academic",
92
+ # 代码
93
+ "github": "code",
94
+ "pypi": "code",
95
+ "npm": "code",
96
+ "crates": "code",
97
+ "mdn": "code",
98
+ "stackoverflow": "code",
99
+ "gitlab": "code",
100
+ "devto": "code",
101
+ "docker_hub": "code",
102
+ "huggingface": "code",
103
+ "local_github": "code",
104
+ "local_gitlab": "code",
105
+ "local_npm": "code",
106
+ "local_stackoverflow": "code",
107
+ # 行情/资金
108
+ "sina_quote": "finance_market",
109
+ "tencent_quote": "finance_market",
110
+ "em_flow": "finance_market",
111
+ "finviz": "finance_market",
112
+ "eastmoney": "finance_market",
113
+ "cninfo": "finance_market",
114
+ "seeking_alpha": "finance_market",
115
+ # 宏观
116
+ "fred": "finance_macro",
117
+ "worldbank": "finance_macro",
118
+ "nbs_stats": "finance_macro",
119
+ "eurostat": "finance_macro",
120
+ "fx_rate": "finance_macro",
121
+ # 快讯
122
+ "cls_telegraph": "news_flash",
123
+ "em_global_news": "news_flash",
124
+ "jin10": "news_flash",
125
+ "em_miaoxiang": "news_flash",
126
+ "local_bing_news": "news_flash",
127
+ "local_google_news": "news_flash",
128
+ # 社区 UGC
129
+ "zhihu": "social",
130
+ "v2ex": "social",
131
+ "juejin": "social",
132
+ "reddit": "social",
133
+ "twitter": "social",
134
+ "fxtwitter": "social",
135
+ "xiaohongshu": "social",
136
+ "bilibili": "social",
137
+ "weibo": "social",
138
+ "hackernews": "social",
139
+ # 热榜
140
+ "baidu_hot": "hot_trending",
141
+ "toutiao_hot": "hot_trending",
142
+ "bilibili_hot": "hot_trending",
143
+ "zhihu_hot": "hot_trending",
144
+ "ths_hot": "hot_trending",
145
+ # 百科/实体
146
+ "wikipedia": "knowledge",
147
+ "zh_wikipedia": "knowledge",
148
+ "baidu_baike": "knowledge",
149
+ "wikidata": "knowledge",
150
+ "moegirl": "knowledge",
151
+ "free_dictionary": "knowledge",
152
+ "local_wikipedia": "knowledge",
153
+ "local_wiktionary": "knowledge",
154
+ "local_wikiquote": "knowledge",
155
+ # 化学/药学
156
+ "pubchem": "science_chem",
157
+ "openfda": "science_chem",
158
+ "clinicaltrials": "science_chem",
159
+ # 生物/蛋白
160
+ "uniprot": "science_bio",
161
+ "rcsb_pdb": "science_bio",
162
+ "gbif": "science_bio",
163
+ # 地球/空间
164
+ "usgs": "science_geo",
165
+ "nasa_cmr": "science_geo",
166
+ "local_openstreetmap": "science_geo",
167
+ "openstreetmap": "science_geo",
168
+ # 法律
169
+ "courtlistener": "legal",
170
+ "wenshu": "legal",
171
+ # 媒体/图书
172
+ "itunes": "media_book",
173
+ "open_library": "media_book",
174
+ "weread": "media_book",
175
+ "douban_book": "media_book",
176
+ "gutenberg": "media_book",
177
+ "musicbrainz": "media_book",
178
+ "openverse": "media_book",
179
+ "imdb": "media_book",
180
+ "local_imdb": "media_book",
181
+ # 体育
182
+ "thesportsdb": "sports",
183
+ # 归档/历史
184
+ "wayback_cdx": "archive",
185
+ "archive_org": "archive",
186
+ # 其他垂直
187
+ "steam": "misc_vertical",
188
+ "polymarket": "misc_vertical",
189
+ "qweather": "misc_vertical",
190
+ "rfc_editor": "misc_vertical",
191
+ "models_dev": "misc_vertical",
192
+ "itotii": "misc_vertical",
193
+ "urban_dictionary": "misc_vertical",
194
+ "know_your_meme": "misc_vertical",
195
+ "coingecko": "misc_vertical",
196
+ # 火车票(官方接口,结构化行数据)
197
+ "train": "misc_vertical",
198
+ # 垂直结构化模态卡(统一语义识别,跨垂直域)
199
+ "bocha_ai": "structured_card",
200
+ # 本地聚合
201
+ "local_search": "web_general",
202
+ "tencent_kline": "finance_market",
203
+ "qq_music": "media_book",
204
+ "usda": "science_bio",
205
+ "opensky": "structured_card",
206
+ "electricity_maps": "misc_vertical",
207
+ "tatoeba": "knowledge",
208
+ "figshare": "academic",
209
+ "searchmysite": "web_general",
210
+ "lieu": "web_general",
211
+ "cnii": "academic",
212
+ "ndl": "knowledge",
213
+ "kor_law": "legal",
214
+ "hatena_bookmark": "social",
215
+ "dnb": "knowledge",
216
+ "doaj": "academic",
217
+ "europeana": "knowledge",
218
+ "hal": "academic",
219
+ "eu_opendata": "misc_vertical",
220
+ "open_meteo": "structured_card",
221
+ "gov_policy": "legal",
222
+ "qiita": "social",
223
+ "fr_opendata": "misc_vertical",
224
+ # ── 补录:此前未归类、靠 DEFAULT_FAMILY 静默落到 web_general 的引擎 ──────
225
+ #
226
+ # 2026-09-16:实测 222 个引擎里有 69 个从未被本表或 config 标注过,于是
227
+ # 全部保底成 web_general(该族一度占 41%)。后果不是「统计计算方式难看」,而是
228
+ # **这些源永远选不中**——route 按 family 组 combo,一个被误判为「全网搜索」
229
+ # 的垂直源在通用查询里被同族上限挤掉,在它真正该服务的垂直域里又不被认作
230
+ # 该族成员。实测:查「生物医学预印本」combo 里没有 biorxiv、查「SEC 监管
231
+ # 文件」没有 sec_edgar、查「CVE 漏洞」没有 nvd,而它们其实都已接入且可用。
232
+ #
233
+ # 归族判据只有一条:**这个源回答的是哪一类问题**。判断依据取自各源 spec 的
234
+ # desc(本表的注释即该源的自我描述摘要),不按数据形态(JSON/HTML)分。
235
+ #
236
+ # 学术文献
237
+ "biorxiv": "academic",
238
+ "openreview": "academic",
239
+ "datacite": "academic",
240
+ "zenodo": "academic",
241
+ "tinyfish_paper": "academic",
242
+ "k10plus": "academic", # 德国最大联合目录:书目/馆藏
243
+ # CORE 也归 academic 而非新建族:它回答的仍是「这个主题有哪些论文」,
244
+ # 与 openalex/crossref 同一问题域;差别在**返回层**(可下载全文 PDF)
245
+ # 而非能力域。单独建族会让 dedupe_by_family 失去「同族去重」的意义。
246
+ "core": "academic",
247
+ # 代码/包
248
+ "deps_dev": "code",
249
+ "endoflife": "code",
250
+ # 百科/实体
251
+ "ror": "knowledge", # 研究机构标识
252
+ "opencorporates": "knowledge",
253
+ "wikisource": "knowledge",
254
+ "zdic": "knowledge", # 汉典:字义/音韵/字源
255
+ # 媒体/图书/艺术
256
+ "bangumi": "media_book",
257
+ "jikan": "media_book",
258
+ "tvmaze": "media_book",
259
+ "douban_movie": "media_book",
260
+ "deezer": "media_book",
261
+ "listenbrainz": "media_book",
262
+ "netease_music": "media_book",
263
+ "local_goodreads": "media_book",
264
+ "artic": "media_book", # 芝加哥艺术博物馆馆藏
265
+ "cleveland": "media_book",
266
+ "met_museum": "media_book",
267
+ "nasa_images": "media_book",
268
+ # 生物/医药
269
+ "obis": "science_bio",
270
+ "worms": "science_bio",
271
+ "iplant": "science_bio", # 中文植物名 → 学名/分类(补 gbif 的中文盲区)
272
+ "who_don": "science_bio", # WHO 疫情暴发通报
273
+ "who_gho": "science_bio", # WHO 全球卫生指标
274
+ # 地球/空间
275
+ "soilgrids": "science_geo",
276
+ "noaa_swpc": "science_geo",
277
+ "satnogs": "science_geo",
278
+ "tle_mirror": "science_geo",
279
+ "gdacs": "science_geo", # 全球多灾种预警
280
+ "carbon_intensity": "science_geo",
281
+ "energy_charts": "science_geo",
282
+ # 法律/标准
283
+ "egov_law": "legal",
284
+ "flk_law": "legal",
285
+ "gov_regulations": "legal",
286
+ "openstd": "legal", # 国标全文公开系统
287
+ "std_samr": "legal", # 全国标准信息公共服务平台
288
+ "nhtsa_vpic": "legal", # 车辆型式认证(法规型数据,非行情)
289
+ # 安全情报(NVD 原被默认成全网搜索,实测 CVE 查询选不中)
290
+ "nvd": "security",
291
+ "crt_sh": "security", # 证书透明度日志:子域名/证书情报
292
+ # 宏观/贸易
293
+ "un_comtrade": "finance_macro",
294
+ "gdelt": "news_flash", # 全球新闻事件数据库
295
+ "people_daily": "news_flash",
296
+ "sspai": "social",
297
+ "redskill": "social",
298
+ "zhihu_hot_app": "hot_trending",
299
+ # 归档
300
+ "marginalia": "web_general", # 独立爬虫索引,确属全网搜索
301
+ "wiby": "web_general", # 老式手工网页索引,同上
302
+ # 独立/新兴全网搜索(显式登记,避免再来一轮静默保底)
303
+ "brave": "web_general",
304
+ "felo": "web_general",
305
+ "firecrawl": "web_general",
306
+ "keenable": "web_general",
307
+ "metaso": "web_general",
308
+ "parallel": "web_general",
309
+ "parallel_free": "web_general",
310
+ # seltz 归 news_flash:它答的是新闻类问题(scope 只有 news/wikipedia/
311
+ # people/companies 四个语料)。原先归 web_general 是误判——该族在
312
+ # dedupe_by_family 里 max_per_family=2,它会与 octen/anysearch 争槽位
313
+ # 而被静默挤掉,路由里永远轮不到。
314
+ "seltz": "news_flash",
315
+ "tinyfish": "web_general",
316
+ "tinyfish_news": "news_flash",
317
+ "you": "web_general",
318
+ "google_news": "news_flash",
319
+ "wolframalpha": "structured_card",
320
+ "google_patents": "misc_vertical",
321
+ "realtime_index": "misc_vertical",
322
+ "gbfs_nyc": "misc_vertical", # 共享单车站点
323
+ "twitter_syndication": "social",
324
+ # 本地新闻流(world_news):每源服务一种本地语言,按查询语言互斥选源。
325
+ # 声明在这里(而非只写在 spec)是因为 route._specs_snapshot() 在 engines
326
+ # 模块未加载时返回空表,族/语言判定必须能从静态表回退,否则路由层看不见。
327
+ "yna": "world_news", "tass": "world_news", "aljazeera": "world_news",
328
+ "elpais": "world_news", "folha": "world_news", "lefigaro": "world_news",
329
+ "faz": "world_news", "nhk": "world_news", "matichon": "world_news",
330
+ "vnpress": "world_news", "antara": "world_news", "hurriyet": "world_news",
331
+ "ynet": "world_news", "ct24": "world_news",
332
+ }
333
+
334
+ # 族 → 展示名
335
+ FAMILY_LABELS: dict[str, str] = {
336
+ "web_general": "全网搜索",
337
+ "academic": "学术文献",
338
+ "code": "代码/包/文档",
339
+ "finance_market": "行情/资金",
340
+ "finance_macro": "宏观数据",
341
+ "news_flash": "快讯/电报",
342
+ "social": "社区 UGC",
343
+ "hot_trending": "热榜",
344
+ "knowledge": "百科/实体",
345
+ "science_chem": "化学/药学",
346
+ "science_bio": "生物/蛋白",
347
+ "science_geo": "地球/空间",
348
+ "legal": "法律判例",
349
+ "media_book": "媒体/图书",
350
+ "sports": "体育",
351
+ "archive": "归档/历史",
352
+ "structured_card": "垂直结构化模态卡",
353
+ "personal_data": "个人数据(本人创作/收藏/关注)",
354
+ "misc_vertical": "其他垂直",
355
+ # 安全情报(CVE/漏洞/证书):与「代码/包」区分开——回答的是「这个组件
356
+ # 有没有已知漏洞」,不是「这个库怎么用」,路由与组合策略也不同。
357
+ "security": "安全情报",
358
+ # 声明核验(事实核查):回答的是「这个具体声明被判定为真/假/误导」,
359
+ # 既不是「发生了什么」(news_flash)也不是「X 是什么」(knowledge)。
360
+ # 单列一族是为了不与新闻源争 max_per_family=2 的同族槽位。
361
+ "verification": "声明核验",
362
+ # 本地新闻流(各国本地语言的一手新闻 RSS):与 hot_trending(中文热榜)
363
+ # 和 news_flash(财经快讯)区分——回答的是「某国此刻发生了什么」。
364
+ # 单列一族有三个理由:①同族 max_per_family=2 会把 20 个语言源互相挤掉;
365
+ # ②语言由 langs 声明、按查询语言选源,需要族内可枚举;
366
+ # ③它是实时流,不能参与通用 combo 回填(同 hot_trending)。
367
+ "world_news": "本地新闻流",
368
+ # 购物(2026-09-26 批次十):电商站内搜索登录墙/强反爬,六源均为 DDG
369
+ # site: 语法包装,回答「某商品在某平台的在售/价格」,不是通用网页检索。
370
+ "shopping": "购物",
371
+ }
372
+
373
+
374
+ def family_of(engine: str, spec: dict[str, Any] | None = None) -> str:
375
+ """返回引擎的能力族。
376
+
377
+ 优先级:config.yaml 引擎声明的 `family` 字段 > 本表显式覆盖 > 默认 web_general。
378
+ spec 传入时优先读 spec["family"](声明式,config 是来源)。
379
+ """
380
+ if spec and isinstance(spec, dict):
381
+ f = spec.get("family")
382
+ if isinstance(f, str) and f:
383
+ return f
384
+ return _ENGINE_FAMILY_OVERRIDES.get(engine, DEFAULT_FAMILY)
385
+
386
+
387
+ # ── 语言维度(2026-09-07 从 route.py 三张手写冻结表忠实推导收紧)──────────
388
+ # 语义:查询语言 lang 下,engine_langs 不含 lang 且不含 "*" → 该引擎对该
389
+ # 语言无召回价值(语言重排移尾 / ja-ko 组合过滤 / research 语言 boost /
390
+ # 可达性门统一从这里取)。config.yaml 引擎声明 `langs` 时以其为准;
391
+ # 未声明的引擎 = ["*"](语言中立:anysearch/github/wikipedia/arxiv/local_* 等)。
392
+ ENGINE_LANGS: dict[str, list[str]] = {
393
+ # 纯英文社区(对中文查询几乎零召回)
394
+ "hackernews": ["en"], "reddit": ["en"], "twitter": ["en"],
395
+ "nitter": ["en"], "lobsters": ["en"],
396
+ # 中文专用(对 en/ja/ko 几乎零召回)
397
+ "zhihu": ["zh"], "zhihu_global": ["zh"], "zhihu_hot": ["zh"],
398
+ "zhihu_user": ["zh"], "zhihu_content": ["zh"],
399
+ "bilibili": ["zh"], "v2ex": ["zh"], "xiaohongshu": ["zh"],
400
+ "weibo": ["zh"], "baidu_baike": ["zh"], "moegirl": ["zh"],
401
+ "cn_encyclopedia": ["zh"], "gov_policy": ["zh"], "cn_ai_news": ["zh"],
402
+ "wechat_sogou": ["zh"], "baidu_hot": ["zh"], "toutiao_hot": ["zh"],
403
+ "bilibili_hot": ["zh"], "juejin": ["zh"], "cnblogs": ["zh"],
404
+ "wenshu": ["zh"], "kor_law": ["ko"],
405
+ # 中英双语(ja/ko 无召回):金融/中文 web API 源
406
+ "bocha": ["zh", "en"], "bocha_ai": ["zh", "en"], "byted": ["zh", "en"],
407
+ "octen": ["zh", "en"], "tencent_kline": ["zh", "en"],
408
+ "eastmoney": ["zh", "en"], "sina_quote": ["zh", "en"],
409
+ "tencent_quote": ["zh", "en"], "em_flow": ["zh", "en"],
410
+ "em_global_news": ["zh", "en"], "em_miaoxiang": ["zh", "en"],
411
+ "jin10": ["zh", "en"], "ths_hot": ["zh", "en"],
412
+ "cls_telegraph": ["zh", "en"], "finviz": ["en"],
413
+ # 本地新闻流:语言绑定(俄语查询用韩联社只会拿到韩语新闻,是错不是次优),
414
+ # 路由据此在族内按查询语言互斥选源。
415
+ "yna": ["ko"], "nhk": ["ja"], "tass": ["ru"], "aljazeera": ["ar"],
416
+ "elpais": ["es"], "folha": ["pt"], "lefigaro": ["fr"], "faz": ["de"],
417
+ "matichon": ["th"], "vnpress": ["vi"], "antara": ["id"],
418
+ "hurriyet": ["tr"], "ynet": ["he"], "ct24": ["cs"],
419
+ # 语言专用的国别垂直库:与上面同理——只在 spec YAML 里声明会被路由层
420
+ # 漏掉(route._specs_snapshot() 在 engines 未加载时返回空表,回退到本表)。
421
+ # 实测:cinii 漏登记时英文查询照样把它选进 academic 域预算窗口。
422
+ "cinii": ["ja"],
423
+ # 同样只存在于 config.yaml 而未登记本表的日语源(route_lang 查
424
+ # ENGINE_LANGS 决定族内互斥与 must_keep,漏登记 = 在语言路由层隐形):
425
+ # qiita 实测日文查询 5/5 全日文,是 ja 技术类查询的主力源;
426
+ # hatena_bookmark 日本技术圈书签聚合,同族。
427
+ # ndl(日本国立国会图书馆书目)**故意不登记**:实测它对任何语言都返回
428
+ # 0 条(上游 OpenSearch 正常 200,builder 已注册,故是解析层坏了)。
429
+ # 登记一个恒返回空的源只会让路由把预算花在死源上。修好解析层再登记。
430
+ "qiita": ["ja"], "hatena_bookmark": ["ja"],
431
+ }
432
+ ENGINE_LANGS_DEFAULT = ("*",)
433
+
434
+ # 书写系统标签 → 该语系下的具体语言码。
435
+ #
436
+ # 为什么需要这张表:lang_detect 对**没有足够特征**的文本只给到书写系统
437
+ # (西里尔/阿拉伯/希伯来/天城文/泰文),不给具体语言——`новости сегодня`
438
+ # 返回 cyrillic、`أخبار اليوم` 返回 arabic。而源声明的是具体语言(tass=ru、
439
+ # aljazeera=ar)。两边对不上时,该语言的本地源会被判成「不匹配」而永远
440
+ # 选不中(实测:俄语/阿语新闻查询 combo 里没有任何本地源)。
441
+ #
442
+ # 展开方向是单向的:查询给语系 → 认该语系下的具体语言源;查询给具体语言
443
+ # (ja/ko/zh 等)时不反向展开——那会把「zh 查询用日文源」这类错配放进来
444
+ # (lang_capability 记过同类错误:ja 展开成 (zh, ja) 会串味)。
445
+ _SCRIPT_FAMILY_LANGS: dict[str, tuple[str, ...]] = {
446
+ "cyrillic": ("ru", "uk", "bg", "sr", "mk", "be"),
447
+ "arabic": ("ar", "fa", "ur"),
448
+ "hebrew": ("he",),
449
+ "thai": ("th",),
450
+ "devanagari": ("hi", "mr", "ne"),
451
+ "greek": ("el",),
452
+ }
453
+
454
+ # 语言提示词:查询里出现这些实词时,按该语言处理。
455
+ #
456
+ # 为什么需要:lang_detect 对拉丁字母语言只给到 en/latin——`noticias de hoy`
457
+ # 判成 en、`berita hari ini` 判成 en、`tin tức` 判成 latin。这些语言共享
458
+ # 字母表,靠码位分不开(与 CJK 的汉字共享同一根因)。结果是西语/葡语/越南语/
459
+ # 印尼语/土耳其语查询在 world_news 族里匹配不到任何源,只能拿到英文通用源,
460
+ # 新接的 elpais/folha/vnpress/antara/hurriyet 永远选不中。
461
+ #
462
+ # 判据是**该语言的新闻/资讯类实词**(与 world_news 域 patterns 同一批词):
463
+ # 这类词在查询里出现,说明用户就是在用那种语言问「有什么新闻」。方向仍然是
464
+ # 单向的——只在路由层做「查询语言 → 该语言源」的候选收窄,不改写查询、
465
+ # 不参与语言能力加权,判定失误的代价仅限于选源顺序。
466
+ _LANG_HINT_WORDS: dict[str, tuple[str, ...]] = {
467
+ "ko": ("뉴스",), "ru": ("новости",), "ar": ("أخبار",),
468
+ "es": ("noticias",), "pt": ("notícias",), "fr": ("actualités", "nouvelles"),
469
+ "de": ("nachrichten",), "ja": ("ニュース",), "th": ("ข่าว",),
470
+ "vi": ("tin tức",), "id": ("berita",), "tr": ("haberler", "haber"),
471
+ "he": ("חדשות",), "cs": ("zprávy",),
472
+ }
473
+
474
+
475
+ def lang_hint_from_query(query: str) -> str:
476
+ """从查询文本里的实词推断语言;无命中返回 ""。
477
+
478
+ 只服务「本地语言源选源」这一个场景,不替代 lang_detect:调用方应在
479
+ 语言判定给不出具体语种(en/latin/mixed)时才用它。
480
+ """
481
+ low = (query or "").lower()
482
+ if not low:
483
+ return ""
484
+ for lang, words in _LANG_HINT_WORDS.items():
485
+ for w in words:
486
+ if w.lower() in low:
487
+ return lang
488
+ return ""
489
+
490
+
491
+ def engine_langs(engine: str, spec: dict[str, Any] | None = None) -> set[str]:
492
+ """引擎语言能力集合。优先级:spec.langs > ENGINE_LANGS > 默认 ["*"]。"""
493
+ if spec and isinstance(spec.get("langs"), list) and spec["langs"]:
494
+ return {str(x) for x in spec["langs"]}
495
+ got = ENGINE_LANGS.get(engine)
496
+ if got:
497
+ return set(got)
498
+ return set(ENGINE_LANGS_DEFAULT)
499
+
500
+
501
+ def lang_allows(engine: str, lang: str, spec: dict[str, Any] | None = None) -> bool:
502
+ """该引擎对 lang 是否有召回价值("*" = 语言中立)。"""
503
+ langs = engine_langs(engine, spec)
504
+ if "*" in langs or lang in langs:
505
+ return True
506
+ # 查询只判到书写系统时,认该语系下的具体语言源(见 _SCRIPT_FAMILY_LANGS)
507
+ return any(code in langs for code in _SCRIPT_FAMILY_LANGS.get(lang, ()))
508
+
509
+
510
+ def engines_not_for_lang(engines: list[str], lang: str,
511
+ specs: dict[str, dict[str, Any]] | None = None) -> list[str]:
512
+ """对 lang 无召回价值的引擎名单(供 ja/ko 组合过滤——剔除中文绑定源)。
513
+
514
+ 规则:langs 不含 lang 且含 "zh"(中文绑定)才剔除;英文/语言中立源
515
+ 保留——忠实原 _JA_KO_CN 语义(ja/ko 用户读英文社区仍有效)。
516
+ """
517
+ out: list[str] = []
518
+ for e in engines:
519
+ spec = (specs or {}).get(e)
520
+ langs = engine_langs(e, spec)
521
+ if "*" in langs or lang in langs:
522
+ continue
523
+ if "zh" in langs:
524
+ out.append(e)
525
+ return out
526
+
527
+
528
+ def engines_demote_for_lang(engines: list[str], lang: str,
529
+ specs: dict[str, dict[str, Any]] | None = None) -> list[str]:
530
+ """语言重排降级名单:对该查询语言无召回价值的源,整体移尾。
531
+
532
+ zh 查询:仅降英文社区类(family=social 对中文零召回——垂直源如
533
+ finviz 对「AAPL 美股盘前」这类中文查询仍完全有效,不降)。
534
+ 其余语言(含 en/ja/ko 与 ru/ar/es/th/vi 等):降**语言专用源**——
535
+ 声明了具体语言、不含查询语言、**且不含 en** 的那些。
536
+
537
+ 2026-09-21 泛化说明:原实现对 en/ja/ko 只降「含 zh 的源」,其余语言
538
+ 完全不降——这在只有中英日源时等价,但接入 14 个本地语言新闻源与
539
+ cinii(ja)这类国别垂直库后就漏了:中文查询会原样保留 cinii(日文论文),
540
+ 英文查询会保留 kor_law(韩文判例)。
541
+
542
+ **「不含 en」这条判据不能省**:英文源是国际通用的(韩语/中文用户读英文
543
+ 社区仍有价值),中文源、日文源则是单语读者专用的。所以
544
+ `["zhihu(zh)", "hackernews(en)"]` 在 ko 查询下要降 zhihu 而保留
545
+ hackernews——若改成「凡不含查询语言就降」,两者同时移尾、相对顺序不变,
546
+ 等于没降(tests/test_review_round3 的 ko/ja 两个用例正是锁这个语义)。
547
+
548
+ 注意:这条只管**顺序**(移尾,让位给匹配源),不删源——删除是
549
+ route._filter_lang_bound_family 对语言绑定族(world_news)的处置,
550
+ 理由见该函数 docstring。
551
+ """
552
+ out: list[str] = []
553
+ for e in engines:
554
+ spec = (specs or {}).get(e)
555
+ langs = engine_langs(e, spec)
556
+ if "*" in langs or lang in langs:
557
+ continue
558
+ if lang == "zh":
559
+ if family_of(e, spec) == "social":
560
+ out.append(e)
561
+ elif "en" not in langs:
562
+ out.append(e)
563
+ return out
564
+
565
+
566
+ def family_candidates(family: str, lang: str = "*",
567
+ *, enabled: set[str] | None = None,
568
+ specs: dict[str, dict[str, Any]] | None = None,
569
+ mode: str = "auto",
570
+ limit: int | None = None) -> list[str]:
571
+ """能力族 × 语言 × 模式 → 可用源排序(分发层单一取源口)。
572
+
573
+ research 语言/学术 boost、recovery 保底、可达性门、DSH 子代理面都应
574
+ 从这里派生——新源注册一次(registry + family/langs)全路径可见,
575
+ 不再有「源存在但任何分发路径都到不了」的死源。priority 升序=优先。
576
+ """
577
+ if specs is None:
578
+ try:
579
+ from config import get_engines
580
+ specs = get_engines(routable_only=(enabled is None))
581
+ except Exception:
582
+ specs = {}
583
+ out: list[str] = []
584
+ for name, spec in (specs or {}).items():
585
+ if not isinstance(spec, dict):
586
+ continue
587
+ if spec.get("enabled") is False or spec.get("explicit_only"):
588
+ continue
589
+ if enabled is not None and name not in enabled:
590
+ continue
591
+ if family_of(name, spec) != family:
592
+ continue
593
+ if lang and lang != "*" and not lang_allows(name, lang, spec):
594
+ continue
595
+ if mode in ("fast", "budget") and \
596
+ (spec.get("cost_tier") or "free") == "paid":
597
+ continue
598
+ out.append(name)
599
+
600
+ def _prio(n: str) -> tuple:
601
+ s = (specs or {}).get(n) or {}
602
+ p = s.get("priority")
603
+ return (p if isinstance(p, (int, float)) else 999, n)
604
+
605
+ out.sort(key=_prio)
606
+ if limit:
607
+ out = out[:limit]
608
+ return out
609
+
610
+
611
+ def family_label(family: str) -> str:
612
+ return FAMILY_LABELS.get(family, family)
613
+
614
+
615
+ def group_by_family(engines: dict[str, dict[str, Any]]) -> dict[str, list[str]]:
616
+ """把引擎字典按能力族分组。"""
617
+ groups: dict[str, list[str]] = {}
618
+ for name, spec in engines.items():
619
+ f = family_of(name, spec)
620
+ groups.setdefault(f, []).append(name)
621
+ return groups
622
+
623
+
624
+ def dedupe_by_family(engine_list: list[str], max_per_family: int = 2,
625
+ spec_lookup: dict[str, dict[str, Any]] | None = None,
626
+ limit_families: frozenset[str] | None = None) -> list[str]:
627
+ """按能力族去重:同族至多保留 max_per_family 个,避免同质源堆叠。
628
+
629
+ 用于 combo 组合后处理:web_general 族有 4 个源时,只留最靠前的 2 个,
630
+ 给其他族腾出预算位。保序去重。
631
+
632
+ limit_families:仅对这些族应用上限(route 层只收缩 web_general,
633
+ 垂直族保留多源交叉验证);None 时对所有族去重。
634
+ """
635
+ spec_lookup = spec_lookup or {}
636
+ counts: dict[str, int] = {}
637
+ out: list[str] = []
638
+ for e in engine_list:
639
+ f = family_of(e, spec_lookup.get(e))
640
+ if limit_families is not None and f not in limit_families:
641
+ out.append(e)
642
+ continue
643
+ if counts.get(f, 0) >= max_per_family:
644
+ continue
645
+ counts[f] = counts.get(f, 0) + 1
646
+ out.append(e)
647
+ return out
648
+
649
+
650
+ # 互补回填排除的族:产出非「查询相关」内容(热榜列表/策展/归档),
651
+ # 回填通用 combo 只会引入噪声,不放行。
652
+ _REFILL_EXCLUDED_FAMILIES = frozenset({
653
+ "misc_vertical",
654
+ "hot_trending",
655
+ "world_news",
656
+ "media_book",
657
+ "archive",
658
+ "personal_data",
659
+ })
660
+
661
+
662
+ def complement_refill(
663
+ combo: list[str],
664
+ *,
665
+ enabled: set[str] | None = None,
666
+ spec_lookup: dict[str, dict[str, Any]] | None = None,
667
+ domain_primary: str | None = None,
668
+ max_slots: int = 2,
669
+ ) -> list[str]:
670
+ """能力互补回填:为「全 web_general」的 combo 追加互补能力族的启用引擎。
671
+
672
+ 语义:family 去重的目的是消灭同质源堆叠,若去重后 combo 里已存在垂直族
673
+ 成员,能力多样性已具备,不再追加(尊重域作者配置)。仅当 combo 全部是
674
+ web_general 时才回填,兑现「给其他族腾出预算位」。
675
+
676
+ 候选规则(保守,宁缺毋滥):
677
+ - 只取 enabled 且不在 combo 中的引擎
678
+ - 族未在 combo 中出现(能力互补,不重复检索方式)
679
+ - 排除 _REFILL_EXCLUDED_FAMILIES(热榜/策展/归档等噪声族)
680
+ - 与域主引擎 coverage 标签至少重叠 1 个(主题相关性的数据信号);
681
+ 主引擎无 coverage 标签时不回填(无主题信号,不猜测)
682
+ - 按 config priority 升序(数字小=优先,与 family_candidates 同计算方式),
683
+ 最多 max_slots 个,保序追加
684
+
685
+ 返回新列表,绝不重排或删减入参 combo。
686
+ """
687
+ if not combo:
688
+ return combo
689
+ if enabled is None or not spec_lookup:
690
+ return combo
691
+ # 已有垂直族成员 → 能力多样性已具备
692
+ if any(family_of(e, spec_lookup.get(e)) != "web_general" for e in combo):
693
+ return combo
694
+ primary_cov = set((spec_lookup.get(domain_primary or "") or {}).get("coverage") or [])
695
+ if not primary_cov:
696
+ return combo
697
+
698
+ represented = {family_of(e, spec_lookup.get(e)) for e in combo}
699
+ candidates: list[tuple[tuple, str]] = []
700
+ for name in enabled:
701
+ if name in combo:
702
+ continue
703
+ spec = spec_lookup.get(name) or {}
704
+ fam = family_of(name, spec)
705
+ if fam in represented or fam in _REFILL_EXCLUDED_FAMILIES:
706
+ continue
707
+ if set(spec.get("coverage") or []) & primary_cov:
708
+ p = spec.get("priority")
709
+ # 缺 priority 视为最差(999),与 family_candidates._prio 一致。
710
+ # 此前是 `or 0` + 降序,且 docstring 写「降序」——三条计算方式互相矛盾,
711
+ # 实际效果是专挑优先级数字最大(最差)的源回填。
712
+ prio = p if isinstance(p, (int, float)) else 999
713
+ candidates.append(((prio, name), name))
714
+ candidates.sort(key=lambda x: x[0])
715
+ return combo + [name for _, name in candidates[:max_slots]]
716
+
717
+
718
+ def describe_families() -> str:
719
+ """调试/文档用:列出所有族及其成员。"""
720
+ lines = []
721
+ for f, label in FAMILY_LABELS.items():
722
+ members = [e for e, ef in _ENGINE_FAMILY_OVERRIDES.items() if ef == f]
723
+ if members:
724
+ lines.append(f" {f} ({label}): {', '.join(sorted(members))}")
725
+ return "\n".join(lines)
726
+
727
+
728
+ if __name__ == "__main__":
729
+ import sys
730
+ sys.path.insert(0, __file__.rsplit("/", 1)[0])
731
+ print("能力族全景:")
732
+ print(describe_families())