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,605 @@
1
+ #!/usr/bin/env python3
2
+ """route_lang.py — 语言判定与「按语言选源」策略的唯一实现。
3
+
4
+ ## 为什么单独成模块
5
+
6
+ 2026-09 的两次线上事故都长在这个交界处:韩语查询被中文内容域吃掉、日文查询
7
+ 落进中文技术源、韩语查询被 TF-IDF 送进韩语法条库。共同点是「语言判定」与
8
+ 「引擎选源」散在两处,改一边忘一边。现在语言相关的知识只有这一个家:
9
+
10
+ - 查询侧:书写系统/主语言判定(`extract_features`)、语言标签、
11
+ 「用英文搜」这类显式语言覆盖;
12
+ - 引擎侧:语言绑定源的筛选与排序(`_filter_lang_bound_family`、
13
+ `_lang_aware_combo_order`、`_merge_language_engines`)、多语言主力源补位
14
+ (`_inject_multilingual_backup`)、must_keep 的语言保护
15
+ (`_lang_must_keep`)。
16
+
17
+ 判据的单一真源仍是 `engine_families`(ENGINE_LANGS / lang_allows /
18
+ engines_not_for_lang)与 `lang_detect`(LANG_LABELS);本模块只做「怎么用」。
19
+
20
+ 依赖方向:route_lang 不导入 route(route 导入它)。需要 route 侧状态的地方
21
+ (如 engines 模块是否已加载)走 `sys.modules` 查询而不是导入。
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ import sys
28
+ from typing import Any
29
+
30
+ from config import get_engines, load_config
31
+ from engine_families import (
32
+ engine_langs,
33
+ engines_demote_for_lang,
34
+ family_of,
35
+ lang_allows,
36
+ lang_hint_from_query,
37
+ )
38
+
39
+ try:
40
+ from argo_engine_registry import get_registry as _get_registry
41
+ except Exception: # 注册中心不可用时语言选源退化为「无本地子引擎」
42
+ _get_registry = None
43
+
44
+
45
+ _RE_CHINESE = re.compile(r"[一-鿿]")
46
+
47
+
48
+ _RE_KANA = re.compile(r"[\u3040-\u30ff]")
49
+
50
+
51
+ _RE_HANGUL = re.compile(r"[\uac00-\ud7af]")
52
+
53
+
54
+ _RE_COMPARE = re.compile(r"\b(vs|versus)\b|(对比|比较|区别|相比|哪个好)", re.I)
55
+
56
+
57
+ _RE_TECH = re.compile(
58
+ r"\b(api|python|javascript|typescript|code|react|vue|node|rust|go|"
59
+ r"golang|docker|kubernetes|linux|git|sql|error|bug|debug|exception|"
60
+ r"function|class|async|thread|database|algorithm|programming|framework|library)\b|"
61
+ r"(函数|方法|类|库|框架|报错|调试|编程|代码|开发|技术|源码|架构)", re.I)
62
+
63
+
64
+ _RE_QUESTION = re.compile(
65
+ r"\b(how|what|why|when|where|which|who)\b|"
66
+ r"(怎么|什么|为什么|如何|哪里|哪个|谁|多少|几|吗|呢)", re.I)
67
+
68
+
69
+ _RE_DEPTH = re.compile(
70
+ r"\b(deep|comprehensive|review|survey|research|paper|thesis)\b|"
71
+ r"(对比分析|深度|全面|详细|深入|系统|完整|综述|研究|探究|详解|论文)", re.I)
72
+
73
+
74
+ _LANG_OVERRIDE_MAP: dict[str, tuple[str, ...]] = {
75
+ "en": ("用英文", "用英语", "in english", "english version", "english only", "英語で"),
76
+ "ja": ("用日文", "用日语", "in japanese", "日本語で", "日本语"),
77
+ "ko": ("用韩文", "用韩语", "in korean", "한국어로"),
78
+ "zh": ("用中文", "用汉语", "in chinese", "中文版"),
79
+ "cyrillic": ("用俄语", "用俄文", "in russian", "по-русски"),
80
+ }
81
+
82
+
83
+ def _detect_lang_override(query: str) -> str | None:
84
+ """检测显式语言覆盖意图,返回目标语言(无则 None)。"""
85
+ if not query:
86
+ return None
87
+ ql = query.lower()
88
+ for lang, keys in _LANG_OVERRIDE_MAP.items():
89
+ for key in keys:
90
+ if key in ql:
91
+ return lang
92
+ return None
93
+
94
+
95
+ def apply_lang_override(features: dict[str, Any],
96
+ langs: list[str]) -> dict[str, Any]:
97
+ """查询操作符 lang: 覆盖自动检测(2026-10-17 query_operators 契约层)。
98
+
99
+ 用户显式声明语言时以声明为准——自动检测对中英混排常失手。只改
100
+ primary_lang/operator_langs 两个键,不重算其余特征;无有效语言码时
101
+ 原样返回(调用方少一次判断)。
102
+ """
103
+ valid = [x.strip().lower() for x in (langs or []) if x and x.strip()]
104
+ if not valid:
105
+ return features
106
+ out = dict(features)
107
+ out["primary_lang"] = valid[0]
108
+ out["operator_langs"] = valid
109
+ return out
110
+
111
+
112
+ def extract_features(query: str) -> dict[str, Any]:
113
+ """提取查询特征向量。
114
+
115
+ P0-001:并入 has_geo / has_negation / intents,供下游路由/并行度决策使用。
116
+ 查询理解不可用时退化为纯正则特征,不影响原有字段。
117
+
118
+ 多语种(v2.7):primary_lang 判定主语言(zh/en/ja/ko/latin/cyrillic/thai/
119
+ arabic/hebrew/greek/devanagari/mixed/other),script 给出书写系统类别,
120
+ is_latin 标记是否为拉丁语系查询。跨语言回退依据:非拉丁书写系统的主语言
121
+ 在通用英文源覆盖可能不足,路由会追加英文通用源(duckduckgo/anysearch)。
122
+ """
123
+ total = len(query)
124
+ chinese = len(_RE_CHINESE.findall(query))
125
+ latin = len(re.findall(r"[A-Za-z]", query))
126
+ ratio = chinese / max(total, 1)
127
+
128
+ # 主语言判定统一走 lang_detect(假名/谚文/西里尔/泰/阿/希伯来/希腊强信号优先)
129
+ primary_lang = "mixed"
130
+ script = "other"
131
+ is_latin = False
132
+ try:
133
+ from lang_detect import detect_language, detect_script
134
+ primary_lang = detect_language(query)
135
+ script = detect_script(query)
136
+ is_latin = primary_lang in ("en", "latin")
137
+ except ImportError:
138
+ kana = len(_RE_KANA.findall(query))
139
+ hangul = len(_RE_HANGUL.findall(query))
140
+ if kana / max(total, 1) >= 0.15:
141
+ primary_lang = "ja"
142
+ script = "kana"
143
+ elif hangul / max(total, 1) >= 0.15:
144
+ primary_lang = "ko"
145
+ script = "hangul"
146
+ elif ratio > 0.3:
147
+ primary_lang = "zh"
148
+ script = "cjk"
149
+ elif latin / max(total, 1) > 0.5:
150
+ primary_lang = "en"
151
+ script = "latin"
152
+ is_latin = True
153
+ else:
154
+ primary_lang = "mixed"
155
+ script = "mixed"
156
+
157
+ features: dict[str, Any] = {
158
+ "chinese_ratio": ratio,
159
+ "english_ratio": latin / max(total, 1),
160
+ "length": total,
161
+ "primary_lang": primary_lang,
162
+ "script": script,
163
+ "is_latin": is_latin,
164
+ # P2-3:显式语言覆盖(「用英文搜」→ en);无覆盖意图时为 None
165
+ "lang_override": _detect_lang_override(query),
166
+ "has_compare": bool(_RE_COMPARE.search(query)),
167
+ "has_technical": bool(_RE_TECH.search(query)),
168
+ "has_question": bool(_RE_QUESTION.search(query)),
169
+ "has_depth_word": bool(_RE_DEPTH.search(query)),
170
+ "has_geo": False,
171
+ "has_negation": False,
172
+ "intents": [],
173
+ }
174
+ try:
175
+ from query_understanding import _understand_cached as understand
176
+ qu = understand(query)
177
+ features["has_geo"] = bool(qu.geo)
178
+ features["has_negation"] = bool(qu.exclude_terms)
179
+ features["intents"] = list(qu.intents)
180
+ except ImportError:
181
+ pass # query_understanding 不可用,保留默认值
182
+ except Exception as e:
183
+ import logging
184
+ logging.getLogger("unified_search.route").debug(
185
+ f"查询理解特征跳过: {type(e).__name__}")
186
+ return features
187
+
188
+
189
+ def _feature_labels(features: dict[str, Any]) -> str:
190
+ labels = []
191
+ # 语言标签取 features.primary_lang(lang_detect 单一真源),**不用
192
+ # chinese_ratio 二分**:后者对谚文/假名/西里尔/阿拉伯查询一律给出
193
+ # chinese_ratio≈0,于是「한국 반도체 산업」这类韩文查询在 route_reason 里
194
+ # 被标成「英文」(2026-09-21 实测)。reason 是给人看的归因入口,标签错了
195
+ # 会把排障引向错误方向(以为命中了英文源)。名称表复用 lang_detect.LANG_LABELS,
196
+ # 不再在这里维护第二份语种名。
197
+ # 阈值语义保持不变:cr>0.6 记中文、0.1~0.6 的混合查询不记语言标签——
198
+ # 这次只修「cr<0.1 却断言是英文」那一条,不改变中英混合的既有形态。
199
+ cr = features.get("chinese_ratio", 0)
200
+ primary = features.get("primary_lang")
201
+ if primary and primary not in ("en", "latin", "mixed", "other"):
202
+ # detect_language 给了明确非英语语种,以它为准。
203
+ # 日语汉字查询(如「人工知能 論文」)cr>0.6 但 detect_language 正确
204
+ # 返回 ja——用 cr 二分把它标成「中文」会把排障引向错误方向。
205
+ labels.append(_lang_label(primary) or "中文")
206
+ elif cr > 0.6:
207
+ labels.append("中文")
208
+ elif cr < 0.1:
209
+ labels.append(_lang_label(features.get("primary_lang")) or "英文")
210
+ for key, name in (("has_technical", "技术向"), ("has_compare", "对比分析"),
211
+ ("has_depth_word", "深度研究"), ("has_question", "问答型")):
212
+ if features.get(key):
213
+ labels.append(name)
214
+ return " + ".join(labels) if labels else "通用查询"
215
+
216
+
217
+ def _lang_label(lang: Any) -> str | None:
218
+ """primary_lang → 中文名;en/latin/混合/未知返回 None(由调用方兜底)。
219
+
220
+ 名称表直接取自 lang_detect(单一真源),**不做进程内缓存**:本函数只在
221
+ 拼 reason 时调用一次,而 extract_features 每次路由本来就会 import
222
+ lang_detect,多一次 sys.modules 查表是零成本;为它维护一个模块级可变
223
+ 缓存只会多一个需要解释的状态。
224
+ """
225
+ if not lang or lang in ("en", "latin", "mixed", "other"):
226
+ return None
227
+ try:
228
+ from lang_detect import LANG_LABELS
229
+ except ImportError: # 语言模块不可用:不编造语种名,交给调用方兜底
230
+ return None
231
+ return LANG_LABELS.get(lang)
232
+
233
+
234
+ def _inject_multilingual_backup(engines_combo: list[str], enabled: set[str],
235
+ features: dict) -> list[str]:
236
+ """ja/ko 查询把多语言主力源 anysearch 送到 combo 前二。
237
+
238
+ 必须在 _apply_engine_policy(预算截断/must_keep 换位)之后调用:此前的
239
+ 注入会被 must_keep 的尾位替换挤出去(2026-09-07 实测 geo 域 anysearch
240
+ 被 local_bing 顶掉)。hedged 执行下 #2 位=primary 慢或不及格时的第一
241
+ 救援;日韩查询过滤掉中文源后常只剩英文/本地源,缺位=无通用主力。
242
+
243
+ 注:这条规则只覆盖 ja/ko。其余语言的同类问题(垂直专源占住 combo 预算、
244
+ 通用保底源被截断剪掉)在声明来源层修,见 config.yaml 各域 engines_combo
245
+ 的顺序约定与 tests/test_combo_budget_coverage.py 检查。
246
+ """
247
+ if features.get("primary_lang") not in ("ja", "ko"):
248
+ return engines_combo
249
+ if "anysearch" not in enabled or not engines_combo:
250
+ return engines_combo
251
+ if "anysearch" in engines_combo:
252
+ if engines_combo.index("anysearch") <= 1:
253
+ return engines_combo
254
+ engines_combo = [e for e in engines_combo if e != "anysearch"]
255
+ return engines_combo[:1] + ["anysearch"] + engines_combo[1:]
256
+
257
+
258
+ def _enabled_local_engines() -> list[str]:
259
+ """返回已注册(enabled)的本地子引擎名。
260
+
261
+ 路由选择的引擎必须能被执行层真正调用。list_local_engines(available_only=False)
262
+ 返回全部子引擎(已按 config.yaml enabled 字段过滤,禁用引擎不入选),
263
+ 若路由选到这些引擎,执行层注册表里不存在 → 「未知引擎」空跑。
264
+ 这里以 config.yaml 的 enabled 字段为准过滤,保证路由与执行一致。
265
+ """
266
+ if _get_registry is None:
267
+ return []
268
+ try:
269
+ from config import get_engines, load_config
270
+ cfg = load_config()
271
+ engines = get_engines(cfg) or {}
272
+ return [
273
+ e for e in _get_registry().list_local_engines(available_only=False)
274
+ if isinstance(engines.get(e), dict) and engines[e].get("enabled", True)
275
+ ]
276
+ except Exception:
277
+ return []
278
+
279
+
280
+ def _select_language_engines(features: dict | None = None) -> list[str]:
281
+ """按查询语言选择应追加的语言本地引擎(P2-1 单一入口,与组合无关)。
282
+
283
+ 多语种(v2.7):按 primary_lang 追加对应语言的本地引擎——
284
+ 中文/日文/韩文 → local_bing(动态 setlang 吃对应语言索引;yandex/
285
+ google 直连 html 引擎已随本机可达性门下线,ddgs 后端路子在子技能侧保留)。
286
+ 只做选择(已按 enabled 过滤、最多 2 个),不碰既有 combo;
287
+ route_query 内一次计算、三处合并共用,杜绝各路径逻辑漂移。
288
+ """
289
+ if _get_registry is None or not features:
290
+ return []
291
+
292
+ primary_lang = features.get("primary_lang", "")
293
+ chinese_ratio = features.get("chinese_ratio", 0)
294
+ sub_engines = _enabled_local_engines()
295
+ if not sub_engines:
296
+ return []
297
+
298
+ # P2-3:显式语言覆盖优先——「用英文搜 苹果」即使含中文也按 en 选引擎
299
+ lang_override = features.get("lang_override")
300
+ if lang_override:
301
+ if lang_override == "ja":
302
+ return [e for e in ["local_bing"] if e in sub_engines][:2]
303
+ if lang_override == "ko":
304
+ return [e for e in ["local_bing"] if e in sub_engines][:2]
305
+ if lang_override == "zh":
306
+ return [e for e in ["local_bing"] if e in sub_engines]
307
+ # en / cyrillic / 其他:动态 setlang 保底多语言索引
308
+ return [e for e in ["local_bing"] if e in sub_engines]
309
+
310
+ # 日/韩:local_bing 动态 setlang 承接(yandex/google 直连引擎已下线,
311
+ # 可达性判决见 references/engines.md;ddgs 后端路子在子技能侧保留)。
312
+ if primary_lang == "ja":
313
+ return [e for e in ["local_bing"] if e in sub_engines][:2]
314
+ if primary_lang == "ko":
315
+ return [e for e in ["local_bing"] if e in sub_engines][:2]
316
+
317
+ if chinese_ratio > 0.1:
318
+ # 只要含中文字符就追加中文引擎(阈值 0.1 覆盖中英混合查询)
319
+ # 百度/搜狗质量低,仅作印证;自动追加只用 local_bing
320
+ return [e for e in ["local_bing"] if e in sub_engines]
321
+ if primary_lang in (
322
+ "cyrillic", "thai", "arabic", "hebrew", "greek", "devanagari",
323
+ ):
324
+ # 其他非拉丁语:local_bing 靠动态 setlang 吃多语言索引
325
+ return [e for e in ["local_bing"] if e in sub_engines]
326
+ if primary_lang in ("mixed", "other", ""):
327
+ # 弱信号:按 lang_pref(习惯/系统/中英基线)选本地引擎
328
+ prefer: list[str] = []
329
+ try:
330
+ from lang_pref import prefer_langs
331
+ prefer = prefer_langs(query_lang=primary_lang)
332
+ except ImportError:
333
+ prefer = ["zh", "en"]
334
+ top = prefer[0] if prefer else "en"
335
+ if top == "ja":
336
+ return [e for e in ["local_bing"] if e in sub_engines]
337
+ if top == "ko":
338
+ return [e for e in ["local_bing"] if e in sub_engines]
339
+ if top == "zh":
340
+ return [e for e in ["local_bing"] if e in sub_engines]
341
+ return [e for e in ["local_bing"] if e in sub_engines]
342
+ if features.get("has_depth_word"):
343
+ return [e for e in ["local_arxiv", "local_semantic_scholar"] if e in sub_engines]
344
+ return []
345
+
346
+
347
+ def _merge_language_engines(engine_list: list[str], features: dict | None,
348
+ lang_engines: list[str]) -> list[str]:
349
+ """把预选语言引擎合并进当前 combo(P2-1,三处共用、重复执行结果一致)。
350
+
351
+ 日/韩:中文域引擎(byted/bocha 等)是噪声源需剔除,且不因「combo 已有
352
+ local_ 引擎」而跳过——中文引擎对日韩查询无用;
353
+ 其他语种:combo 已含 local_ 引擎则跳过(避免与 _expand_local_search 重复追加)。
354
+ """
355
+ if not features:
356
+ return engine_list
357
+ result = list(engine_list)
358
+ # P2-3:显式语言覆盖 ja/ko 与主语言 ja/ko 同等对待(噪声剔除同一套规则)
359
+ if (features.get("lang_override") or features.get("primary_lang")) in ("ja", "ko"):
360
+ cn_noise = {"bocha", "byted", "wechat_sogou", "zhihu", "zhihu_global", "baidu_baike"}
361
+ result = [e for e in result if e not in cn_noise]
362
+ for eng in lang_engines[:2]:
363
+ if eng not in result:
364
+ result.append(eng)
365
+ return result
366
+ # 已包含 local_ 引擎则跳过
367
+ if any(e.startswith("local_") for e in result):
368
+ return result
369
+ for eng in lang_engines[:2]:
370
+ if eng not in result:
371
+ result.append(eng)
372
+ return result
373
+
374
+
375
+ def _add_language_engines(engine_list: list[str], features: dict | None = None) -> list[str]:
376
+ """为已路由的查询添加语言相关的本地引擎(补充源,兼容入口)。
377
+
378
+ route_query 内已改为「先 _select_language_engines 一次、再
379
+ _merge_language_engines 三处共用」;本函数保留独立调用能力(重复执行结果一致),
380
+ 供外部或未来调用方使用。
381
+ """
382
+ return _merge_language_engines(engine_list, features, _select_language_engines(features))
383
+
384
+
385
+ _SOCIAL_ZH_GENERAL = ("anysearch", "local_bing")
386
+
387
+
388
+ def _specs_snapshot() -> dict:
389
+ """引擎 spec 快照(读 config langs 覆盖);不可用时回退 ENGINE_LANGS 表。
390
+
391
+ **已加载才读,不为它去导入。** `engines` 连带 engines_base → urllib/http.client
392
+ 整条 HTTP 栈;而它模块级的 `_engine_specs` 只在 registry 被 `_load_registry()`
393
+ 填充,route 从不加载 registry——旧写法(`from engines import _engine_specs`)
394
+ 付了整笔导入的钱,拿回的却是一个空表,路由结果与「不导入」逐位相同
395
+ (222 引擎全量比对:0 处差异;config 里声明 langs 的只有 6 个引擎,而
396
+ 回退的 ENGINE_LANGS 表有 42 条,空表反而走的是更全的那条路)。
397
+ engines 真被导入过时(dispatch / available_engines 之后)两者取到的是
398
+ 同一个 dict 对象,语义不变。
399
+ """
400
+ mod = sys.modules.get("engines")
401
+ if mod is None:
402
+ return {}
403
+ return getattr(mod, "_engine_specs", None) or {}
404
+
405
+
406
+ def _move_to_tail(combo: list[str], excluded) -> list[str]:
407
+ """combo 中命中 excluded 的引擎稳定移尾(其余保持原顺序)。"""
408
+ excl = set(excluded)
409
+ keep = [e for e in combo if e not in excl]
410
+ tail = [e for e in combo if e in excl]
411
+ return keep + tail
412
+
413
+
414
+ def _lang_aware_combo_order(combo: list[str], features: dict | None,
415
+ domain_name: str | None,
416
+ enabled: set[str],
417
+ query: str = "") -> list[str]:
418
+ """语言感知的 combo 排序(返回新列表,不改动传入)。
419
+
420
+ 双向对称:
421
+ zh 查询:纯英文社区引擎稳定移尾;social 域把通用中文 web 源提到最前。
422
+ en/ja/ko 查询:中文专用引擎稳定移尾(patterns 认关键词不认语言,
423
+ 英文查询命中 social 域时 zhihu 排头会垄断 budget + 早停)。
424
+ 排序发生在 engine_policy 截断之前,保证 budget 截断保留的是对查询
425
+ 语言有用的引擎。
426
+ """
427
+ if not combo or not features:
428
+ return combo
429
+ zh_ratio = features.get("chinese_ratio") or 0
430
+ primary_lang = features.get("primary_lang")
431
+ # 汉字占比保底不得命中 ja/ko:假名/谚文文本里的汉字同属 CJK 区段,
432
+ # 纯 ratio 判定会把日文查询(汉字占比常 >0.15)误入中文分支,
433
+ # 中文专用源占前排——与 query_rewriter 的语言门控计算方式一致。
434
+ is_zh = primary_lang == "zh" or (
435
+ primary_lang not in ("ja", "ko") and zh_ratio > 0.15)
436
+ if is_zh:
437
+ ordered = _move_to_tail(
438
+ combo, engines_demote_for_lang(combo, "zh", _specs_snapshot()))
439
+ if domain_name == "social":
440
+ for g in _SOCIAL_ZH_GENERAL:
441
+ if g in enabled and g in ordered:
442
+ ordered = [g] + [e for e in ordered if e != g]
443
+ break
444
+ if domain_name == "zhihu_content" and "zhihu_global" in ordered:
445
+ # 站内主搜 + 站外全网搜是成对语义:learner 同族按分重排会把
446
+ # zhihu_global 挪到 anysearch 之后,叠加 auto 预算=2 即被截掉
447
+ # (37 天仅 53 次的死因)。zh 查询下固定提回 #2。
448
+ rest = [e for e in ordered if e not in ("zhihu", "zhihu_global")]
449
+ if "zhihu" in ordered:
450
+ ordered = ["zhihu", "zhihu_global"] + rest
451
+ else:
452
+ ordered = ["zhihu_global"] + rest
453
+ # 对称分支:非中文查询把中文专用源移尾(含 zh_ratio≤0.15 的混合查询)
454
+ elif primary_lang in ("en", "ja", "ko"):
455
+ ordered = _move_to_tail(
456
+ combo, engines_demote_for_lang(
457
+ combo, primary_lang or "en", _specs_snapshot()))
458
+ else:
459
+ ordered = combo
460
+ # 其余语言(ru/ar/es/pt/th/vi/…)此前完全不做降级:en/ja/ko/zh 在上方
461
+ # 分支处理过,这里补上,否则 cinii(声明 ja)在中文查询里、kor_law
462
+ # (声明 ko)在英文查询里都会占着原位。
463
+ _lang = _family_lang(features, query)
464
+ if _lang and _lang not in ("zh", "en", "ja", "ko", "mixed", "other"):
465
+ ordered = _move_to_tail(
466
+ ordered, engines_demote_for_lang(ordered, _lang, _specs_snapshot()))
467
+ # 语言绑定的族(本地新闻流)按查询语言互斥——见函数 docstring 末段。
468
+ return _filter_lang_bound_family(ordered, features, _specs_snapshot(), query)
469
+
470
+
471
+ def _family_lang(features: dict | None, query: str = "") -> str:
472
+ """选源用的查询语言:lang_override > primary_lang > 查询实词兜底。
473
+
474
+ 第三档专门补拉丁字母语言的判定缺口(lang_detect 对 noticias/berita/
475
+ tin tức 只给 en/latin),见 engine_families._LANG_HINT_WORDS 的说明。
476
+ """
477
+ lang = (features or {}).get("lang_override") or (features or {}).get("primary_lang") or ""
478
+ if lang in ("", "en", "latin", "mixed", "other"):
479
+ hint = lang_hint_from_query(query)
480
+ if hint:
481
+ return hint
482
+ return lang
483
+
484
+
485
+ def _filter_lang_bound_family(combo: list[str], features: dict | None,
486
+ specs: dict[str, dict[str, Any]] | None,
487
+ query: str = "") -> list[str]:
488
+ """语言绑定族的成员按查询语言互斥:匹配语言的留原位,其余整体移尾。
489
+
490
+ 与 hot_trending 不同,world_news 族(各国本地新闻流)是**语言绑定**的:
491
+ 俄语查询用韩联社只会拿到韩语新闻,不是「次优」而是「错」。同族内每个
492
+ 源各自服务一种语言,所以选源的第一判据是查询语言,不是 priority。
493
+
494
+ **非匹配源一律摘除,不做「移尾保留」**——这是与 engines_demote_for_lang
495
+ 有意不同的一处。那条路径处理的是「同一份内容的不同语言版本」(降级),
496
+ 这条处理的是「完全不同的国家与语言」:俄语查询跑韩联社拿回的是韩语新闻,
497
+ 这不是次优而是错。实测 deep 档(不截断预算)下移尾方案会让韩语查询连跑
498
+ elpais/tass/aljazeera 等 16 个源,白付网络与配额。
499
+
500
+ 摘除的代价是「语言判定失误时拿不到该语言源」,但 combo 里始终有语言中立
501
+ 的兜底源(anysearch),不会零结果;判定不确定(mixed/other/空)时整个族
502
+ 不动,按原样保留。
503
+
504
+ 语言取 lang_override 优先(「用韩语搜 X」的显式意图胜过文本推断),
505
+ 与 _select_language_engines / _lang_must_keep 同一口径。
506
+
507
+ specs 允许为空:_specs_snapshot() 在 engines 模块未加载时返回 {}(性能
508
+ 设计,见该函数 docstring),此时 family_of / lang_allows 会回退
509
+ engine_families 的静态表——新源在那边也声明了一次,所以这里不依赖 spec。
510
+ """
511
+ if not combo or not features:
512
+ return combo
513
+ lang = _family_lang(features, query)
514
+ if not lang or lang in ("mixed", "other"):
515
+ return combo
516
+ keep: list[str] = []
517
+ for eng in combo:
518
+ spec = specs.get(eng)
519
+ # 两类源在非匹配语言时摘除,不是移尾:
520
+ # ① 语言绑定族成员(world_news):每个源服务一种语言;
521
+ # ② 语言独占源(_LANG_EXCLUSIVE_ENGINES,如 cinii=ja)。
522
+ # 与 engines_demote_for_lang 的分工——那条只管顺序,这条管「该不该
523
+ # 在场」:移尾在源本来就排末位时等于没动(实测 cinii 在 academic 域
524
+ # 末位,英文查询照样进预算窗口)。
525
+ lang_bound = family_of(eng, spec) == "world_news"
526
+ lang_exclusive = eng in _LANG_EXCLUSIVE_ENGINES
527
+ if (lang_bound or lang_exclusive) and not lang_allows(eng, lang, spec):
528
+ continue
529
+ keep.append(eng)
530
+ # 族内非匹配成员一律摘除:一个匹配的都没有时退化成「整个族摘掉」(该族
531
+ # 服务不了这个查询——域 patterns 误伤,或语言不在这 14 种里);有匹配时
532
+ # 摘掉的是「别的语言的一手源」。两种情形都不返回它们。通用源仍在 keep
533
+ # 里兜底,不会零结果。
534
+ return keep
535
+
536
+
537
+ _LANG_EXCLUSIVE_ENGINES: frozenset[str] = frozenset({
538
+ "cinii", # 日本学术总库(ja):中文/英文查询拿不到任何可用结果
539
+ })
540
+
541
+
542
+ _LANG_PREFERRED_ENGINES: dict[str, list[str]] = {
543
+ # 保持 local_bing 在首位:它带动态 setlang,是 ja/ko 的通用兜底通道,
544
+ # 且 tests/test_multilingual.py::test_route_query_override_ja_no_cn_noise
545
+ # 把「日文覆盖查询的 combo 必含 local_bing/local_yandex」钉成契约。
546
+ # 本函数只返回 preferred[:1],所以排第一的会挤掉其余——把 hatena 放前面
547
+ # 会让该契约失败(实测 combo 变成 [redskill, hatena_bookmark])。
548
+ #
549
+ # hatena_bookmark(日文实测 5/5 全日文、40 条候选)改为通过
550
+ # _LANG_AUX_ENGINES 作为**补充**注入,不占 must_keep 席位。
551
+ "ja": ["local_bing"],
552
+ "ko": ["local_bing"],
553
+ }
554
+
555
+ # 语言查询的补充引擎:在 must_keep 之外、combo 末尾注入,补首选源覆盖不到的面。
556
+ # 与 _LANG_PREFERRED_ENGINES 的区别见 route_combo.lang_aux_engines 的 docstring。
557
+ _LANG_AUX_ENGINES: dict[str, list[str]] = {
558
+ # ja:qiita 只对技术类有效且召回差(搜「プロンプト」返回 ROCm/Copilot
559
+ # 文章,被噪声门判 noise 丢掉),hatena_bookmark 是日文技术圈书签聚合,
560
+ # 对日文长尾能给出真实结果。
561
+ "ja": ["hatena_bookmark"],
562
+ }
563
+
564
+
565
+ def _lang_must_keep(features: dict | None, enabled: set[str],
566
+ combo: list[str] | None = None,
567
+ query: str = "") -> list[str]:
568
+ """返回语言相关的 must_keep 引擎。
569
+
570
+ 三档判据,优先级从高到低:
571
+
572
+ 1. **语言绑定的本地源**(world_news 族中匹配查询语言的成员)。这类源是
573
+ 该语言的唯一一手通道,而通用 SERP(local_bing)只是二手转述——韩语
574
+ 新闻查询被 budget 裁到 2 位时,该保的是韩联社。没有它,本地语言源
575
+ 接进来也永远进不了预算窗口(实测:ko 查询 combo 被裁成
576
+ [anysearch, local_bing],yna 在窗口外)。
577
+ 2. **语言独占源**(_LANG_EXCLUSIVE_ENGINES,如 cinii=ja)。这类源是
578
+ 该语言在垂直域(如学术)的唯一通道,排在 combo 末位时会被 budget
579
+ 裁掉——实测日语学术查询「人工知能 論论文」cinii 在位次 7,auto/balanced
580
+ budget=3 下永远不可达。与 world_news 同理:没有它,该语言的垂直域
581
+ 等于没源可用。
582
+ 3. 日/韩的通用本地引擎(yandex/google/bing)。专用源默认 disabled 时
583
+ 落到 local_bing;多语言结果质量仍靠 engines_base 动态 setlang。
584
+ """
585
+ if not features or not enabled:
586
+ return []
587
+ # P2-3:显式语言覆盖(用日文搜/用韩语搜)与主语言同等进入 must_keep
588
+ lang = _family_lang(features, query)
589
+ if combo and lang:
590
+ bound = [e for e in combo if e in enabled
591
+ and family_of(e, None) == "world_news"
592
+ and lang_allows(e, lang, None)]
593
+ if bound:
594
+ return bound[:1]
595
+ if combo and lang:
596
+ exclusive = [e for e in combo if e in enabled
597
+ and e in _LANG_EXCLUSIVE_ENGINES
598
+ and lang_allows(e, lang, None)]
599
+ if exclusive:
600
+ return exclusive
601
+ preferred = _LANG_PREFERRED_ENGINES.get(lang, [])
602
+ for eng in preferred:
603
+ if eng in enabled:
604
+ return [eng]
605
+ return []
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """route_log.py — 路由决策采样落日志(旁路,失败静默,仅本机)。
3
+
4
+ 按采样率把「features 齐全」的决策点落一条使用日志记录(P2-6)。单独成模块的理由:
5
+ 它是路由模块**唯一**的写副作用(除决策缓存外),且必须永远 fail-open——
6
+ 使用日志写失败绝不能让一次搜索失败。把它隔离出来,route_query 里就只剩编排。
7
+
8
+ `ARGO_ROUTE_SAMPLE_RATE` 控制采样率(默认 1/20)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from typing import Any
15
+
16
+
17
+ def _env_int(name: str, default: int, minimum: int = 1) -> int:
18
+ """读整型环境变量,任何异常都退回 default。
19
+
20
+ 为什么必须有:这一行原本裸写 `int(os.environ.get(...))`,于是
21
+ `ARGO_ROUTE_SAMPLE_RATE=abc` 在**模块导入期**抛 ValueError,经
22
+ route.py 的导入链直接把整个 `argo search` 打挂——与本模块 docstring
23
+ 写的「必须永远 fail-open,使用日志写失败绝不能让一次搜索失败」正好相反,
24
+ 而且触发它的只是一个拼错的采样率。mcp_handlers 早有同名 _env_int,
25
+ 两处各写各的才是这个 bug 的成因。
26
+ """
27
+ try:
28
+ return max(minimum, int(str(os.environ.get(name, "")).strip() or default))
29
+ except (TypeError, ValueError):
30
+ return default
31
+
32
+
33
+ _ROUTE_SAMPLE_RATE = _env_int("ARGO_ROUTE_SAMPLE_RATE", 20)
34
+ _route_sample_counter = 0
35
+
36
+
37
+ def sample_route(done: dict[str, Any], kw: dict[str, Any]) -> None:
38
+ """按采样率把路由决策落一条使用日志记录。"""
39
+ global _route_sample_counter
40
+ if "features" not in kw or not kw.get("features"):
41
+ return # engine_override 直通等无语义分支不采样
42
+ _route_sample_counter += 1
43
+ if _route_sample_counter % _ROUTE_SAMPLE_RATE != 0:
44
+ return
45
+ f = kw.get("features") or {}
46
+ try:
47
+ from usage_log import emit
48
+ emit("route", {
49
+ "domain": kw.get("domain"),
50
+ "engine": kw.get("engine"),
51
+ "engines": kw.get("engines"),
52
+ "confidence": kw.get("confidence"),
53
+ "mode": kw.get("mode"),
54
+ "lang_override": f.get("lang_override"),
55
+ "primary_lang": f.get("primary_lang"),
56
+ "script": f.get("script"),
57
+ "has_compare": f.get("has_compare"),
58
+ "has_technical": f.get("has_technical"),
59
+ "chinese_ratio": f.get("chinese_ratio"),
60
+ "intents": f.get("intents"),
61
+ })
62
+ except Exception:
63
+ pass