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,334 @@
1
+ #!/usr/bin/env python3
2
+ """citations.py — DOI 元数据获取与四种引用格式化(argo cite 子命令的库层)。
3
+
4
+ 数据源(均免密钥):
5
+ - Crossref https://api.crossref.org/works/{doi}
6
+ - OpenAlex https://api.openalex.org/works/https://doi.org/{doi}
7
+
8
+ 路由:Crossref 优先(卷期页/出版商最全);OpenAlex 是唯一落点,见 fetch_metadata。
9
+ 两个 API 事实决定了这里的写法:Crossref polite pool 只要求 UA 带 mailto 联系方式
10
+ (不收费、不发密钥);OpenAlex 完全免 key。
11
+
12
+ 归一化:OpenAlex work 被折成 Crossref message 的形状(from_openalex_work),
13
+ 四种格式化器只面对一种数据形状(统一 meta 字典)。差异集中在三处,全在
14
+ from_openalex_work 消化:作者只有 display_name(姓氏虚词切分见
15
+ split_display_name)、venue 可能是预印本仓库(前缀命中即按预印本处理,
16
+ 见 _preprint_server)、doi 是完整 URL。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import re
23
+ import urllib.error
24
+ import urllib.parse
25
+ import urllib.request
26
+
27
+ # HTTP 出口统一走 http_open:全仓 urlopen 门禁(test_http_attribution)要求
28
+ # 所有出站请求过同一出口——代理解析(net_proxy)与失败归因只在那一处做。
29
+ # engine="cite" 让归因寄存器有落点(cite 不是引擎,寄存器按名聚合即可)。
30
+ from engines_base import http_open # noqa: E402
31
+
32
+ # polite pool 的 UA:项目级联系方式 + 仓库地址。GitHub noreply 形式的项目邮箱,
33
+ # 与参考实现(dsh-lit-search)同一惯例——Crossref 只要 UA 里有一个可联系的字符串。
34
+ _UA = "argo-cite/1.0 (+https://github.com/taxueseek/argo; mailto:argo-cite@users.noreply.github.com)"
35
+ _API_TIMEOUT = 15.0
36
+
37
+ _CROSSREF_WORKS = "https://api.crossref.org/works/"
38
+ _OPENALEX_WORKS = "https://api.openalex.org/works/"
39
+
40
+ # arXiv 的 DataCite DOI 前缀:Crossref 不收录,命中就直接走 OpenAlex,
41
+ # 不发那记注定 404 的 Crossref 请求。
42
+ _ARXIV_DOI_PREFIX = "10.48550/"
43
+
44
+ # OpenAlex 认作预印本仓库的 source.display_name(小写前缀匹配)。这些是仓库
45
+ # 不是期刊,命中后 venue 置空、类型按预印本走([EB/OL] / @misc 变体)。
46
+ _PREPRINT_SERVERS = (
47
+ "arxiv", "biorxiv", "medrxiv", "ssrn", "chemrxiv",
48
+ "research square", "preprints.org", "techrxiv",
49
+ )
50
+
51
+ # 姓氏虚词(nobility particles):属于 family,不属于 given。大小写不敏感。
52
+ _PARTICLES = frozenset((
53
+ "de", "van", "von", "der", "den", "di", "da", "del", "della",
54
+ "las", "le", "bin", "ibn",
55
+ ))
56
+
57
+ # OpenAlex/Crossref 偶尔在 title/venue 里塞 HTML 实体("A & B Journal")。
58
+ # 单趟替换:&amp;lt; 解码成 &lt; 而不是 <,不做二次解码。
59
+ _ENTITY_MAP = {"&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'"}
60
+ _ENTITY_RE = re.compile(r"&(?:amp|lt|gt|quot|#39);")
61
+
62
+ # 合法 DOI:10. + 4~9 位注册局前缀 + / + 非空白后缀。只做形状校验,不碰网络。
63
+ _DOI_RE = re.compile(r"^10\.\d{4,9}/\S+$")
64
+
65
+
66
+ class CiteError(Exception):
67
+ """获取/格式化失败。消息给用户看,必须说清是哪一步、哪个 DOI。"""
68
+
69
+
70
+ def decode_entities(s: str) -> str:
71
+ return _ENTITY_RE.sub(lambda m: _ENTITY_MAP[m.group(0)], s)
72
+
73
+
74
+ def split_display_name(name: str) -> dict:
75
+ """OpenAlex display_name("given family" 顺序)拆成 family/given。
76
+
77
+ 连续的尾部虚词整体归 family:"Diego de Las Casas" → family
78
+ "de Las Casas"、given "Diego"。单词名整体当 family。
79
+ """
80
+ name = name.strip()
81
+ words = [w for w in name.split() if w]
82
+ if len(words) < 2:
83
+ return {"family": name, "given": ""}
84
+ i = len(words) - 1
85
+ while i > 0 and words[i - 1].lower() in _PARTICLES:
86
+ i -= 1
87
+ return {"family": " ".join(words[i:]), "given": " ".join(words[:i])}
88
+
89
+
90
+ def _preprint_server(raw_venue: str) -> str | None:
91
+ """OpenAlex source 是已知预印本仓库时返回规范化仓库名,否则 None。
92
+
93
+ arXiv 给固定标签(OpenAlex 名它是 "arXiv (Cornell University)",直接展示
94
+ 会把机构带进 howpublished);其余仓库保留 display_name 原样。
95
+ """
96
+ v = raw_venue.strip()
97
+ if not v:
98
+ return None
99
+ for p in _PREPRINT_SERVERS:
100
+ if v.lower().startswith(p):
101
+ return "arXiv" if p == "arxiv" else v
102
+ return None
103
+
104
+
105
+ def _from_crossref_message(msg: dict, doi: str) -> dict:
106
+ title = decode_entities(str((msg.get("title") or [""])[0]))
107
+ container = decode_entities(str((msg.get("container-title") or [""])[0]))
108
+ publisher = decode_entities(str(msg.get("publisher") or ""))
109
+ year = ""
110
+ # published 缺失时 issued 兜底:Crossref 契约里 issued 是必有项,
111
+ # published 只有部分记录带(跨年更正、预印本转正等记录二者会不同)。
112
+ for key in ("published", "issued"):
113
+ parts = (msg.get(key) or {}).get("date-parts") or [[None]]
114
+ y = (parts[0] or [None])[0]
115
+ if y:
116
+ year = str(y)
117
+ break
118
+ return {
119
+ "doi": str(msg.get("DOI") or doi),
120
+ "authors": [
121
+ {"family": str(a.get("family") or ""), "given": str(a.get("given") or "")}
122
+ for a in (msg.get("author") or [])
123
+ ],
124
+ "title": title,
125
+ "container": container,
126
+ "year": year,
127
+ "volume": str(msg.get("volume") or ""),
128
+ "issue": str(msg.get("issue") or ""),
129
+ "pages": str(msg.get("page") or ""),
130
+ "publisher": publisher,
131
+ "type": str(msg.get("type") or ""),
132
+ "url": str(msg.get("URL") or f"https://doi.org/{doi}"),
133
+ "preprint_server": None, # Crossref 路径不会出现 arXiv DOI,见 fetch_metadata
134
+ }
135
+
136
+
137
+ def from_openalex_work(work: dict) -> dict:
138
+ """OpenAlex work → Crossref-message 形状的统一 meta(差异清单见模块头)。"""
139
+ raw_venue = str(((work.get("primary_location") or {}).get("source") or {})
140
+ .get("display_name") or "")
141
+ server = _preprint_server(raw_venue)
142
+ venue = "" if server else raw_venue
143
+ biblio = work.get("biblio") or {}
144
+ first, last = biblio.get("first_page"), biblio.get("last_page")
145
+ if first and last:
146
+ pages = f"{first}-{last}"
147
+ else:
148
+ pages = str(first or last or "")
149
+ doi = re.sub(r"^https?://doi\.org/", "", str(work.get("doi") or ""), flags=re.I)
150
+ return {
151
+ "doi": doi,
152
+ "authors": [split_display_name(((a.get("author") or {}).get("display_name")) or "")
153
+ for a in (work.get("authorships") or [])
154
+ if isinstance(a.get("author") or {}, dict)],
155
+ "title": decode_entities(str(work.get("title") or "")),
156
+ "container": decode_entities(venue),
157
+ "year": str(work.get("publication_year") or ""),
158
+ "volume": str(biblio.get("volume") or ""),
159
+ "issue": str(biblio.get("issue") or ""),
160
+ "pages": pages,
161
+ "publisher": "", # OpenAlex 的 host 机构要额外查表,参考实现不取,这里保持一致
162
+ # 无 venue 的记录按预印本处理:预印本仓库 venue 已被置空,真正的纯
163
+ # 预印本(无正式出处)也归入此类——两种走同一套 [EB/OL] / @misc 变体。
164
+ "type": "journal-article" if venue else "posted-content",
165
+ "url": f"https://doi.org/{doi}" if doi else "",
166
+ "preprint_server": server,
167
+ }
168
+
169
+
170
+ def _get_json(url: str) -> object:
171
+ """GET 并解析 JSON。失败抛 CiteError:带状态码/原因,不吞、不猜。
172
+
173
+ 网络层走统一出口 http_open(全仓 urlopen 门禁;代理解析与失败归因
174
+ 单点在那条链上)。http_open 是等价替换:成功路径同一响应对象,
175
+ HTTPError 原样透传且错误体可再 read——下方 except 分支语义不变。
176
+ 打桩点 = citations.http_open(读取处)。
177
+ """
178
+ req = urllib.request.Request(
179
+ url, headers={"User-Agent": _UA, "Accept": "application/json"})
180
+ try:
181
+ with http_open(req, timeout=_API_TIMEOUT, engine="cite") as resp:
182
+ body = resp.read().decode("utf-8", errors="replace")
183
+ except urllib.error.HTTPError as e:
184
+ try:
185
+ body = e.read().decode("utf-8", errors="replace").strip()
186
+ # 只取首行:上游 404 的错误体可能是整页 HTML,多行会把 CLI 的
187
+ # 错误输出撑成错误屏;状态码 + 首行足够归因。
188
+ detail = body.splitlines()[0][:120] if body else ""
189
+ except Exception:
190
+ detail = "" # HTTP 错误体读取失败(非 UTF-8 / 连接中断),退回空 detail
191
+ raise CiteError(f"HTTP {e.code}: {detail}".rstrip()) from None
192
+ except urllib.error.URLError as e:
193
+ raise CiteError(f"连接失败: {getattr(e, 'reason', e)}") from None
194
+ except OSError as e:
195
+ raise CiteError(f"连接失败: {e}") from None
196
+ try:
197
+ return json.loads(body)
198
+ except json.JSONDecodeError as e:
199
+ raise CiteError(f"响应不是有效 JSON: {e}") from None
200
+
201
+
202
+ def _clean_doi(doi: str) -> str:
203
+ d = doi.strip()
204
+ if not _DOI_RE.match(d):
205
+ raise CiteError(f"不是合法 DOI: {doi!r}(应形如 10.1234/abcdef)")
206
+ return d
207
+
208
+
209
+ def fetch_metadata(doi: str) -> dict:
210
+ """DOI → 统一 meta。Crossref 优先,OpenAlex 兜底(见模块头路由说明)。"""
211
+ d = _clean_doi(doi)
212
+ if not d.lower().startswith(_ARXIV_DOI_PREFIX):
213
+ try:
214
+ payload = _get_json(_CROSSREF_WORKS + urllib.parse.quote(d, safe=""))
215
+ msg = payload.get("message") if isinstance(payload, dict) else None
216
+ if not isinstance(msg, dict):
217
+ raise CiteError("Crossref 响应缺少 message 字段")
218
+ return _from_crossref_message(msg, d)
219
+ except CiteError:
220
+ pass # 查无此 DOI / 网络失败都落 OpenAlex:单源失败不代表 DOI 不存在
221
+ try:
222
+ work = _get_json(
223
+ _OPENALEX_WORKS + "https://doi.org/" + urllib.parse.quote(d, safe=""))
224
+ except CiteError as e:
225
+ raise CiteError(f"Crossref 与 OpenAlex 均未查到 {d}({e})") from None
226
+ if not isinstance(work, dict):
227
+ raise CiteError(f"OpenAlex 响应不是对象: {d}")
228
+ meta = from_openalex_work(work)
229
+ if not meta["doi"]:
230
+ meta["doi"] = d # OpenAlex 个别记录缺 doi 字段,用请求值兜底
231
+ return meta
232
+
233
+
234
+ # GB/T 7714 的文献类型标识。未知类型回落 J(期刊)是行业惯例里最安全的默认。
235
+ _GBT_TYPE_MARKS = {
236
+ "journal-article": "J", "proceedings-article": "C", "monograph": "M",
237
+ "dissertation": "D", "posted-content": "EB",
238
+ }
239
+
240
+ _BIBTEX_TYPES = {"journal-article": "article", "proceedings-article": "inproceedings"}
241
+
242
+
243
+ def _short_name(n: dict) -> str:
244
+ """GB/T 7714 作者形态:"Family G"(取 given 首字母,无句点)。"""
245
+ return f"{n['family']} {n['given'][:1]}".strip()
246
+
247
+
248
+ def _authors_gbt(meta: dict) -> str:
249
+ ns = meta["authors"]
250
+ # 过滤空名(OpenAlex 个别 authorship 缺 display_name),避免产出 ", ," 碎片
251
+ head = ", ".join(s for s in (_short_name(n) for n in ns[:3]) if s)
252
+ return f"{head}, et al" if len(ns) > 3 else head
253
+
254
+
255
+ def _authors_apa(meta: dict) -> str:
256
+ def fmt(n: dict) -> str:
257
+ initials = " ".join(w[0] + "." for w in n["given"].split() if w)
258
+ return ", ".join(p for p in (n["family"], initials) if p)
259
+ ns = meta["authors"]
260
+ head = ", ".join(fmt(n) for n in ns[:3])
261
+ return f"{head}, et al." if len(ns) > 3 else head
262
+
263
+
264
+ def _bibtex_key(meta: dict, used_keys: set | None) -> str:
265
+ """「第一作者姓+年份」键:非法字符剥掉,批内重复按 a/b/c 递增。"""
266
+ family = meta["authors"][0]["family"] if meta["authors"] else ""
267
+ base = re.sub(r"[^a-z0-9]", "", family.lower()) or "unknown"
268
+ base += str(meta.get("year") or "")
269
+ key = base
270
+ if used_keys is not None:
271
+ i = 0
272
+ while key in used_keys:
273
+ i += 1
274
+ # 同族键超过 26 个时字母翻倍(aa/aaa):只会出现在病态批里,兜住即可
275
+ key = base + "abcdefghijklmnopqrstuvwxyz"[(i - 1) % 26] * ((i - 1) // 26 + 1)
276
+ used_keys.add(key)
277
+ return key
278
+
279
+
280
+ def format_citation(meta: dict, style: str, used_keys: set | None = None) -> str:
281
+ """统一 meta → 引用条目。纯函数(used_keys 传入时承担批内 BibTeX 键去重)。
282
+
283
+ style:gbt7714 | gbt7714n(顺序编码制,即带 [1] 序号)| apa | bibtex。
284
+ """
285
+ doi = meta.get("doi") or ""
286
+ title = meta.get("title") or ""
287
+ venue = meta.get("container") or meta.get("publisher") or ""
288
+ year = str(meta.get("year") or "")
289
+
290
+ if style == "gbt7714n":
291
+ return "[1] " + format_citation(meta, "gbt7714")
292
+ if style == "gbt7714":
293
+ authors = _authors_gbt(meta)
294
+ if meta.get("type") == "posted-content" and not venue:
295
+ # 纯预印本走电子资源形态;有年份时按 GB/T 7714 附加更新日期
296
+ y = f" ({year})." if year else ""
297
+ head = f"{authors}. " if authors else ""
298
+ return f"{head}{title}[EB/OL].{y} https://doi.org/{doi}."
299
+ mark = _GBT_TYPE_MARKS.get(meta.get("type") or "", "J")
300
+ vol = "".join(p for p in (meta.get("volume") or "",
301
+ f"({meta['issue']})" if meta.get("issue") else "") if p)
302
+ tail = ", ".join(p for p in (venue, year, vol) if p)
303
+ head = f"{authors}. " if authors else ""
304
+ return f"{head}{title}[{mark}]. {tail}."
305
+ if style == "apa":
306
+ authors = _authors_apa(meta)
307
+ head = f"{authors} " if authors else ""
308
+ parts = [
309
+ f"{head}({year or 'n.d.'}). {title}.",
310
+ f"{venue}." if venue else "",
311
+ f"https://doi.org/{doi}" if doi else "",
312
+ ]
313
+ return " ".join(p for p in parts if p)
314
+ if style == "bibtex":
315
+ ns = meta["authors"]
316
+ entry = _BIBTEX_TYPES.get(meta.get("type") or "", "misc")
317
+ lines = [
318
+ f" author = {{{' and '.join(', '.join(p for p in (n['family'], n['given']) if p) for n in ns)}}}",
319
+ f" title = {{{title}}}",
320
+ ]
321
+ if venue:
322
+ lines.append(f" {'journal' if entry == 'article' else 'booktitle'} = {{{venue}}}")
323
+ if year:
324
+ lines.append(f" year = {{{year}}}")
325
+ if meta.get("volume"):
326
+ lines.append(f" volume = {{{meta['volume']}}}")
327
+ if meta.get("issue"):
328
+ lines.append(f" number = {{{meta['issue']}}}")
329
+ if meta.get("type") == "posted-content" and not venue:
330
+ lines.append(f" howpublished = {{{meta.get('preprint_server') or 'arXiv'} preprint}}")
331
+ if doi:
332
+ lines.append(f" doi = {{{doi}}}")
333
+ return f"@{entry}{{{_bibtex_key(meta, used_keys)},\n" + ",\n".join(lines) + "\n}"
334
+ raise CiteError(f"未知引用格式: {style!r}(可选 gbt7714/gbt7714n/apa/bibtex)")
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """cite_cli.py — argo cite 子命令:DOI → 引用条目直出。
3
+
4
+ 用法:argo cite DOI [DOI...] [--style gbt7714|gbt7714n|apa|bibtex] [--json]
5
+
6
+ 多条 DOI 逐条独立处理:单条失败不中断其余(失败走 stderr / JSON error 项,
7
+ 退出码 1)。引用格式化与元数据获取在 citations.py,本文件只做参数面与输出面。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from citations import CiteError, fetch_metadata, format_citation
16
+ from cli_io import dumps
17
+
18
+ STYLES = ("gbt7714", "gbt7714n", "apa", "bibtex")
19
+ DEFAULT_STYLE = "gbt7714"
20
+
21
+
22
+ def _run(dois: list[str], style: str, as_json: bool) -> int:
23
+ """逐条取元数据并格式化。返回退出码:全部成功 0,任一失败 1。"""
24
+ failed = False
25
+ used_keys: set[str] = set() # bibtex 批内键去重:同一批输出不撞键
26
+ results: list[dict] = []
27
+ for doi in dois:
28
+ try:
29
+ meta = fetch_metadata(doi)
30
+ citation = format_citation(meta, style, used_keys=used_keys)
31
+ except CiteError as e:
32
+ failed = True
33
+ if as_json:
34
+ results.append({"doi": doi, "error": str(e)})
35
+ else:
36
+ print(f"错误: {doi}: {e}", file=sys.stderr)
37
+ continue
38
+ if as_json:
39
+ results.append({"doi": doi, "style": style, "citation": citation})
40
+ else:
41
+ print(citation)
42
+ if as_json:
43
+ print(dumps(results))
44
+ return 1 if failed else 0
45
+
46
+
47
+ def main(argv: list[str] | None = None) -> int:
48
+ ap = argparse.ArgumentParser(
49
+ prog="argo cite", description="DOI → 引用条目(GB/T 7714 / 顺序编码 / APA / BibTeX)")
50
+ ap.add_argument("dois", nargs="+", help="DOI,可多个(空格分隔)")
51
+ ap.add_argument("--style", choices=STYLES, default=DEFAULT_STYLE,
52
+ help="引用格式(默认 gbt7714)")
53
+ ap.add_argument("--json", action="store_true", help="JSON 输出")
54
+ args = ap.parse_args(argv)
55
+ return _run(args.dois, args.style, args.json)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ sys.exit(main())
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """cjk_tokens — 中英混排查询的信号切分(长中文串按重叠 2-gram 展开)。
3
+
4
+ ## 为什么需要它
5
+
6
+ 全仓有多处「取查询 token,再用 `token in blob` 判断结果与查询是否相关」的门。
7
+ 它们此前都用同一个正则:
8
+
9
+ re.findall(r"[A-Z]{2,}|[a-zA-Z]{3,}|[\\u4e00-\\u9fff]{2,}", q)
10
+
11
+ 对**无空格中文**,`[\\u4e00-\\u9fff]{2,}` 会把整句吃成一个 token
12
+ (「年最好看的科幻电影」→ `['年最好看的科幻电影']`)。结果里永远不会出现整句
13
+ 原样,于是 `token in blob` 恒假、**真实结果一条都过不了门**。实测每查询取通用
14
+ 引擎 5 条实回:
15
+
16
+ 查询 实回 旧判据通过 本模块
17
+ 年最好看的科幻电影 5 0 5
18
+ 量子计算最新进展 5 0 5
19
+ 怎么学好深度学习 5 0 5
20
+ 冬奥会金牌榜 5 1 5
21
+ 东京旅游攻略推荐 5 0 5
22
+ 新能源汽车电池技术 5 2 5
23
+
24
+ 失效面不止 recovery 兜底:百度百科相关度门、化学 token 交集、V2EX 中文节点
25
+ 反查用的是同一个正则,四处一起静默失效——中文查询的兜底链路整体形同不存在。
26
+
27
+ ## 判据
28
+
29
+ 长中文串展开为**重叠 2-gram**(「科幻电影」→ 科幻/幻电/电影),再按调用方给的
30
+ 停用词表逐个过滤。等价于「查询与文本的最长公共中文子串 ≥ 2 字」:比整段包含
31
+ 宽松(可满足),比单字严格(挡掉「上」「大」这类噪声)。
32
+
33
+ 停用词过滤是判据的另一半,不能省:不滤的话「最新」会在任何含「最新的XX」的
34
+ 无关页面上命中,门就从「恒假」翻到「恒真」。
35
+
36
+ 英文沿用原语义:`[A-Z]{2,}` 整词大写缩写、`[a-zA-Z]{3,}` 三字母以上词,一律
37
+ 小写后比对。**但缩写不受停用词限制**——「who」本身就在停用词表里,滤掉会让
38
+ 「WHO headquarters」一个信号都不剩,反而把门放成恒真。单字中文不成词
39
+ (与原 `{2,}` 一致)。
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import re
45
+
46
+ __all__ = ["CJK_RE", "SIGNAL_RE", "cjk_ngrams", "cjk_term_grams", "signal_tokens"]
47
+
48
+ #: 信号切分:大写缩写整词 / 三字母以上词 / 中文连续串。
49
+ SIGNAL_RE = re.compile(r"[A-Z]{2,}|[a-zA-Z]{3,}|[\u4e00-\u9fff]+")
50
+ #: 仅中文连续串。
51
+ CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
52
+
53
+ #: 中文成词下限,与历史判据 `[\u4e00-\u9fff]{2,}` 一致——单字不成词。
54
+ MIN_CJK = 2
55
+
56
+
57
+ def cjk_ngrams(run: str, n: int = MIN_CJK) -> list[str]:
58
+ """中文串 → 重叠 n-gram;不长于 n 时原样返回(长度门槛由调用方把)。"""
59
+ if len(run) <= n:
60
+ return [run]
61
+ return [run[i:i + n] for i in range(len(run) - n + 1)]
62
+
63
+
64
+ def cjk_term_grams(text: str, n: int = MIN_CJK) -> list[str]:
65
+ """只取中文串的 n-gram——供「用中文查中文标题」的调用方(如 V2EX 节点反查)。"""
66
+ return [g for run in CJK_RE.findall(text or "")
67
+ if len(run) >= MIN_CJK for g in cjk_ngrams(run, n)]
68
+
69
+
70
+ def signal_tokens(text: str, stop: "frozenset | set | None" = None,
71
+ n: int = MIN_CJK) -> list[str]:
72
+ """查询/文本 → 信号 token 列表(全部小写,已按 `stop` 过滤)。
73
+
74
+ Args:
75
+ text: 查询或待比对文本。
76
+ stop: 停用词表;命中即丢弃。中文按 n-gram 逐个比对,所以停用词写
77
+ 「最新」能同时挡掉 bigram「最新」。
78
+ n: 中文 n-gram 宽度。
79
+ """
80
+ stop = stop or frozenset()
81
+ out: list[str] = []
82
+ for tok in SIGNAL_RE.findall(text or ""):
83
+ if CJK_RE.fullmatch(tok):
84
+ if len(tok) < MIN_CJK:
85
+ continue # 单字不成词
86
+ out.extend(g for g in cjk_ngrams(tok, n) if g not in stop)
87
+ continue
88
+ low = tok.lower()
89
+ # 大写缩写(WHO/NASA)不受停用词限制:历史语义如此,而且不能改——
90
+ # 「who」本身就在停用词表里,滤掉会让「WHO headquarters」一个信号都不剩,
91
+ # 于是 `if not keys: return True` 把门放成恒真。test_p0_v25 的 Chegg
92
+ # 反例锁的就是这一点。
93
+ if (tok.isupper() and len(tok) >= 2) or low not in stop:
94
+ out.append(low)
95
+ return out
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- clarify.py — 意图消歧工具(wigolo query_understanding 理念移植)
3
+ clarify.py — 意图消歧工具
4
4
 
5
5
  核心能力:
6
6
  1. 歧义检测:识别查询中的多义词
@@ -17,10 +17,10 @@ clarify.py — 意图消歧工具(wigolo query_understanding 理念移植)
17
17
  from __future__ import annotations
18
18
 
19
19
  import argparse
20
- import json
21
20
  import re
22
21
  import sys
23
22
  from typing import Any
23
+ from cli_io import dumps, ensure_utf8_stdio
24
24
 
25
25
 
26
26
  # ── 歧义词库 ──────────────────────────────────────────────────────────────────
@@ -32,9 +32,11 @@ AMBIGUOUS_TERMS = {
32
32
  {"text": "Apple 公司(科技/股票)", "domain": "tech", "weight": 0.6},
33
33
  {"text": "苹果(水果/食品)", "domain": "general", "weight": 0.3},
34
34
  {"text": "苹果(操作系统/macOS)", "domain": "tech", "weight": 0.1},
35
+ {"text": "苹果公司(金融/行情)", "domain": "finance", "weight": 0.5},
35
36
  ],
36
37
  "disambiguation_keywords": {
37
- "tech": ["股价", "股票", "iPhone", "Mac", "iOS", "WWDC", "市值", "AAPL", "库克", "蒂姆"],
38
+ "tech": ["iPhone", "Mac", "iOS", "WWDC", "库克", "蒂姆", "发布会", "系统", "手机", "平板"],
39
+ "finance": ["股价", "股票", "市值", "AAPL", "财报", "营收", "利润", "行情", "涨跌", "美股", "纳指"],
38
40
  "food": ["吃", "水果", "营养", "减肥", "种植", "产地", "品种"],
39
41
  },
40
42
  },
@@ -447,7 +449,15 @@ def analyze_query(query: str) -> dict[str, Any]:
447
449
 
448
450
  # 歧义检测
449
451
  for term, info in AMBIGUOUS_TERMS.items():
450
- if term in query or (term.isascii() and re.search(r'\b' + re.escape(term) + r'\b', query, re.I)):
452
+ # 英文歧义词必须吃词边界,中文必须**不**吃(CJK 全是 \w,词边界永不成立)。
453
+ #
454
+ # 此前写成 `term in query or (term.isascii() and \b...)`:裸子串那一支
455
+ # 排在前面且对英文同样成立,于是词边界分支永远轮不到——整个判据退化成
456
+ # 子串匹配。实测误报(2026-09-19):`JavaScript 教程` 报「Java」歧义、
457
+ # `Google 发布 Gemini` 报「Go」、`STORAGE 引擎对比` 报「RAG」,并据此
458
+ # 翻转 recommended_strategy(把直搜改成 split_search)。
459
+ if (term in query if not term.isascii()
460
+ else re.search(r'\b' + re.escape(term) + r'\b', query, re.I)):
451
461
  # 检查上下文关键词
452
462
  matched_meanings = []
453
463
  for meaning in info["meanings"]:
@@ -555,23 +565,36 @@ def analyze_query(query: str) -> dict[str, Any]:
555
565
 
556
566
 
557
567
  def _detect_language(text: str) -> str:
558
- """检测主要语言。"""
559
- cn_count = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
560
- en_count = sum(1 for c in text if c.isascii() and c.isalpha())
561
- ja_count = sum(1 for c in text if "\u3040" <= c <= "\u30ff")
562
-
563
- total = cn_count + en_count + ja_count
564
- if total == 0:
565
- return "unknown"
566
-
567
- if cn_count / total > 0.5:
568
- return "zh" if en_count / total < 0.3 else "zh-en"
569
- elif ja_count / total > 0.3:
570
- return "ja" if cn_count / total < 0.3 else "zh-ja"
571
- elif en_count / total > 0.5:
572
- return "en"
573
- else:
568
+ """检测主要语言(委托 lang_detect 单来源,兼容旧标签)。"""
569
+ try:
570
+ from lang_detect import detect_language
571
+ lang = detect_language(text or "")
572
+ except ImportError:
573
+ cn_count = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
574
+ en_count = sum(1 for c in text if c.isascii() and c.isalpha())
575
+ ja_count = sum(1 for c in text if "\u3040" <= c <= "\u30ff")
576
+ total = cn_count + en_count + ja_count
577
+ if total == 0:
578
+ return "unknown"
579
+ if cn_count / total > 0.5:
580
+ return "zh" if en_count / total < 0.3 else "zh-en"
581
+ if ja_count / total > 0.3:
582
+ return "ja" if cn_count / total < 0.3 else "zh-ja"
583
+ if en_count / total > 0.5:
584
+ return "en"
574
585
  return "mixed"
586
+ # 兼容 clarify 下游可能依赖的旧混合标签
587
+ if lang == "other":
588
+ return "unknown"
589
+ if lang == "zh" and any(c.isascii() and c.isalpha() for c in (text or "")):
590
+ han = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
591
+ if han and han / max(len(text.replace(" ", "")), 1) < 0.7:
592
+ return "zh-en"
593
+ if lang == "ja" and any("\u4e00" <= c <= "\u9fff" for c in (text or "")):
594
+ kana = sum(1 for c in text if "\u3040" <= c <= "\u30ff")
595
+ if kana / max(len([c for c in text if not c.isspace()]), 1) < 0.3:
596
+ return "zh-ja"
597
+ return lang
575
598
 
576
599
 
577
600
  # ── 路由推荐 ──────────────────────────────────────────────────────────────────
@@ -603,7 +626,7 @@ def recommend_routing(analysis: dict[str, Any]) -> dict[str, Any]:
603
626
  routing["mode"] = "auto"
604
627
 
605
628
  elif strategy == "news_priority":
606
- routing["engines"] = ["byted", "uapi", "duckduckgo"]
629
+ routing["engines"] = ["byted", "uapi"]
607
630
  routing["explanation"] = "检测到新闻/动态意图,优先使用新闻引擎"
608
631
  routing["mode"] = "fast"
609
632
 
@@ -624,7 +647,7 @@ def recommend_routing(analysis: dict[str, Any]) -> dict[str, Any]:
624
647
  routing["engines"] = ["anysearch"]
625
648
  routing["explanation"] = "检测到 CVE 编号,使用安全垂直域"
626
649
  elif "search_tech" in intents:
627
- routing["engines"] = ["github", "duckduckgo", "uapi"]
650
+ routing["engines"] = ["github", "uapi"]
628
651
  routing["explanation"] = "检测到技术意图,优先技术源"
629
652
  elif "search_opinion" in intents:
630
653
  routing["engines"] = ["zhihu", "byted"]
@@ -639,9 +662,9 @@ def recommend_routing(analysis: dict[str, Any]) -> dict[str, Any]:
639
662
  # ── CLI ───────────────────────────────────────────────────────────────────────
640
663
 
641
664
  def main():
665
+ ensure_utf8_stdio() # 入口第一件事:钉住 stdout/stderr 编码(见 cli_io)
642
666
  parser = argparse.ArgumentParser(description="意图消歧工具")
643
667
  parser.add_argument("query", help="搜索查询")
644
- parser.add_argument("--explain", action="store_true", help="详细解释")
645
668
  parser.add_argument("--json", action="store_true", help="JSON 输出")
646
669
  args = parser.parse_args()
647
670
 
@@ -650,7 +673,7 @@ def main():
650
673
  analysis["routing"] = routing
651
674
 
652
675
  if args.json:
653
- print(json.dumps(analysis, ensure_ascii=False, indent=2))
676
+ print(dumps(analysis))
654
677
  else:
655
678
  print(f"\n查询分析:{analysis['query']}")
656
679
  print(f"语言:{analysis['language']} | 置信度:{analysis['confidence']:.2f}")