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,298 @@
1
+ #!/usr/bin/env python3
2
+ """evidence_tier.py — 证据分层系统
3
+
4
+ 四级证据分层(A/B/C/D),用于量化内容来源的可信度。
5
+ A 级:官方当前公开来源或法律权威文件
6
+ B 级:有明确日期的知名第三方报告或公开媒体
7
+ C 级:品牌自述但缺乏运营细节
8
+ D 级:未验证或市场特定边界项
9
+
10
+ 设计原则:
11
+ - 纯规则、零依赖、微秒级
12
+ - 与 content_signals.classify_source 互补:后者判域名类型,本模块判证据等级
13
+ - 输出可直接用于内容质量评分和来源账本
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime, timezone
20
+ from typing import Any
21
+ from urllib.parse import urlparse
22
+
23
+
24
+ # ── 证据等级 ────────────────────────────────────────────────────────
25
+
26
+ @dataclass(frozen=True)
27
+ class EvidenceTier:
28
+ """证据等级常量"""
29
+ A = "official" # 官方来源
30
+ B = "third_party" # 知名第三方
31
+ C = "self_claim" # 品牌自述
32
+ D = "unverified" # 未验证
33
+
34
+
35
+ # 等级权重(用于加权评分)
36
+ TIER_WEIGHTS = {
37
+ EvidenceTier.A: 1.0,
38
+ EvidenceTier.B: 0.7,
39
+ EvidenceTier.C: 0.3,
40
+ EvidenceTier.D: 0.0,
41
+ }
42
+
43
+ # 等级排序(用于比较)
44
+ TIER_RANK = {
45
+ EvidenceTier.A: 4,
46
+ EvidenceTier.B: 3,
47
+ EvidenceTier.C: 2,
48
+ EvidenceTier.D: 1,
49
+ }
50
+
51
+
52
+ # ── 域名 → 证据等级映射 ────────────────────────────────────────────
53
+
54
+ # A 级:官方来源
55
+ _A_DOMAINS = frozenset([
56
+ # 政府
57
+ "gov.cn", "gov.uk", "gov", "gob.mx", "go.jp", "go.kr",
58
+ # 教育
59
+ "edu", "edu.cn", "ac.uk", "ac.jp", "ac.kr",
60
+ # 官方文档
61
+ "docs.microsoft.com", "developer.apple.com", "developers.google.com",
62
+ "docs.python.org", "readthedocs.io",
63
+ # 官方标准
64
+ "w3.org", "ietf.org", "iso.org", "ieee.org",
65
+ ])
66
+
67
+ # B 级:知名第三方
68
+ _B_DOMAINS = frozenset([
69
+ # 新闻媒体
70
+ "nytimes.com", "bbc.com", "bbc.co.uk", "reuters.com", "theguardian.com",
71
+ "washingtonpost.com", "bloomberg.com", "apnews.com", "aljazeera.com",
72
+ "cnbc.com", "ft.com", "economist.com", "techcrunch.com", "theverge.com",
73
+ "arstechnica.com", "wired.com", "nature.com", "science.org",
74
+ # 中文媒体
75
+ "xinhuanet.com", "people.com.cn", "chinanews.com.cn", "cctv.com",
76
+ "caixin.com", "163.com", "sohu.com", "sina.com.cn", "qq.com",
77
+ "36kr.com", "huxiu.com", "tmtpost.com", "leiphone.com",
78
+ # 研究机构
79
+ "mckinsey.com", "bcg.com", "bain.com", "deloitte.com", "pwc.com",
80
+ "ey.com", "kpmg.com", "accenture.com", "gartner.com", "forrester.com",
81
+ "idc.com", "statista.com", "pewresearch.org", "brookings.edu",
82
+ # 开源社区
83
+ "github.com", "gitlab.com", "stackoverflow.com", "stackexchange.com",
84
+ "medium.com", "substack.com", "wordpress.com",
85
+ # 百科
86
+ "wikipedia.org", "baike.baidu.com", "zhihu.com",
87
+ ])
88
+
89
+ # C 级:品牌自述(需要进一步判断是否有运营细节)
90
+ _C_PATTERNS = [
91
+ r"^blog\.", # 企业博客
92
+ r"^news\.", # 企业新闻
93
+ r"^press\.", # 企业公关
94
+ r"^about\.", # 企业介绍
95
+ r"^company\.", # 企业信息
96
+ ]
97
+
98
+
99
+ @dataclass
100
+ class EvidenceAssessment:
101
+ """证据评估结果"""
102
+ tier: str # 证据等级 A/B/C/D
103
+ tier_weight: float # 等级权重
104
+ source_type: str # 来源类型
105
+ is_official: bool # 是否官方
106
+ has_date: bool = False # 是否有明确日期
107
+ has_author: bool = False # 是否有明确作者
108
+ has_citation: bool = False # 是否有引用/参考
109
+ notes: list[str] = field(default_factory=list) # 评估说明
110
+
111
+
112
+ def assess_evidence_tier(
113
+ url: str,
114
+ source_type: str = "",
115
+ is_official: bool = False,
116
+ has_date: bool = False,
117
+ has_author: bool = False,
118
+ has_citation: bool = False,
119
+ content: str = "",
120
+ ) -> EvidenceAssessment:
121
+ """评估内容的证据等级。
122
+
123
+ 参数:
124
+ url: 内容 URL
125
+ source_type: 来源类型(来自 classify_source)
126
+ is_official: 是否官方来源
127
+ has_date: 是否有明确发布日期
128
+ has_author: 是否有明确作者
129
+ has_citation: 是否有引用/参考来源
130
+ content: 内容文本(用于进一步判断)
131
+
132
+ 返回:
133
+ EvidenceAssessment 对象
134
+ """
135
+ host = _extract_host(url)
136
+ notes = []
137
+
138
+ # 1. 域名匹配
139
+ if _is_a_tier_domain(host):
140
+ tier = EvidenceTier.A
141
+ notes.append(f"官方域名: {host}")
142
+ elif _is_b_tier_domain(host):
143
+ tier = EvidenceTier.B
144
+ notes.append(f"知名第三方域名: {host}")
145
+ elif _is_c_tier_pattern(host):
146
+ tier = EvidenceTier.C
147
+ notes.append(f"企业自述域名: {host}")
148
+ else:
149
+ tier = EvidenceTier.D
150
+ notes.append(f"未知域名: {host}")
151
+
152
+ # 2. 内容信号修正
153
+ # 有明确日期 + 有作者 + 有引用 → 升级
154
+ if tier == EvidenceTier.C and has_date and has_author and has_citation:
155
+ tier = EvidenceTier.B
156
+ notes.append("企业自述但有完整证据链(日期+作者+引用),升级为 B 级")
157
+
158
+ # 无日期 + 无作者 → 降级
159
+ if tier == EvidenceTier.B and not has_date and not has_author:
160
+ tier = EvidenceTier.C
161
+ notes.append("第三方来源但缺乏日期和作者,降级为 C 级")
162
+
163
+ # 3. 内容质量信号
164
+ if content:
165
+ content_signals = _assess_content_signals(content)
166
+ if content_signals["has_data"] and content_signals["has_citation"]:
167
+ if tier == EvidenceTier.C:
168
+ tier = EvidenceTier.B
169
+ notes.append("内容含数据和引用,升级为 B 级")
170
+ elif content_signals["is_thin"]:
171
+ if tier == EvidenceTier.B:
172
+ tier = EvidenceTier.C
173
+ notes.append("内容单薄,降级为 C 级")
174
+
175
+ return EvidenceAssessment(
176
+ tier=tier,
177
+ tier_weight=TIER_WEIGHTS[tier],
178
+ source_type=source_type,
179
+ is_official=is_official,
180
+ has_date=has_date,
181
+ has_author=has_author,
182
+ has_citation=has_citation,
183
+ notes=notes,
184
+ )
185
+
186
+
187
+ def _extract_host(url: str) -> str:
188
+ """提取主机名"""
189
+ if not url:
190
+ return ""
191
+ try:
192
+ host = urlparse(url).netloc.lower()
193
+ if "@" in host:
194
+ host = host.rsplit("@", 1)[1]
195
+ if ":" in host:
196
+ host = host.split(":", 1)[0]
197
+ return host
198
+ except Exception:
199
+ return ""
200
+
201
+
202
+ def _is_a_tier_domain(host: str) -> bool:
203
+ """判断是否 A 级域名"""
204
+ if not host:
205
+ return False
206
+ # 精确匹配
207
+ if host in _A_DOMAINS:
208
+ return True
209
+ # 后缀匹配
210
+ for domain in _A_DOMAINS:
211
+ if host.endswith("." + domain):
212
+ return True
213
+ # 特殊后缀
214
+ if host.endswith(".gov") or host.endswith(".edu") or host.endswith(".ac.uk"):
215
+ return True
216
+ return False
217
+
218
+
219
+ def _is_b_tier_domain(host: str) -> bool:
220
+ """判断是否 B 级域名"""
221
+ if not host:
222
+ return False
223
+ if host in _B_DOMAINS:
224
+ return True
225
+ # 支持子域名匹配(如 www.reuters.com 匹配 reuters.com)
226
+ for domain in _B_DOMAINS:
227
+ if host.endswith("." + domain):
228
+ return True
229
+ return False
230
+
231
+
232
+ def _is_c_tier_pattern(host: str) -> bool:
233
+ """判断是否 C 级模式(企业自述)"""
234
+ if not host:
235
+ return False
236
+ for pattern in _C_PATTERNS:
237
+ if re.search(pattern, host):
238
+ return True
239
+ return False
240
+
241
+
242
+ def _assess_content_signals(content: str) -> dict:
243
+ """评估内容信号"""
244
+ if not content:
245
+ return {"has_data": False, "has_citation": False, "is_thin": True}
246
+
247
+ # 是否有数据(数字、百分比、日期)
248
+ has_data = bool(re.search(r"\d+[\d,\.]*[%%]?", content))
249
+ # 是否有引用(URL、参考文献标记)
250
+ has_citation = bool(re.search(r"(https?://|参考文献|references|资料来源)", content, re.IGNORECASE))
251
+ # 是否单薄(字数少、句子少)
252
+ word_count = len(content)
253
+ # 计算句子数(按标点符号分割)
254
+ sentence_count = len(re.findall(r'[.!?。!?]', content))
255
+ # 单薄判定:字数少于 200 或句子数少于 3
256
+ is_thin = word_count < 200 or sentence_count < 3
257
+
258
+ return {
259
+ "has_data": has_data,
260
+ "has_citation": has_citation,
261
+ "is_thin": is_thin,
262
+ }
263
+
264
+
265
+ def compute_weighted_score(assessments: list[EvidenceAssessment]) -> dict:
266
+ """计算加权证据评分
267
+
268
+ 参数:
269
+ assessments: 证据评估结果列表
270
+
271
+ 返回:
272
+ 包含加权平均分、等级分布、总体评级的字典
273
+ """
274
+ if not assessments:
275
+ return {
276
+ "weighted_score": 0.0,
277
+ "tier_distribution": {},
278
+ "overall_tier": EvidenceTier.D,
279
+ "assessment_count": 0,
280
+ }
281
+
282
+ total_weight = sum(a.tier_weight for a in assessments)
283
+ weighted_score = total_weight / len(assessments)
284
+
285
+ # 等级分布
286
+ tier_dist = {}
287
+ for a in assessments:
288
+ tier_dist[a.tier] = tier_dist.get(a.tier, 0) + 1
289
+
290
+ # 总体评级:取最低等级(短板效应)
291
+ min_tier = min(assessments, key=lambda a: TIER_RANK[a.tier]).tier
292
+
293
+ return {
294
+ "weighted_score": round(weighted_score, 3),
295
+ "tier_distribution": tier_dist,
296
+ "overall_tier": min_tier,
297
+ "assessment_count": len(assessments),
298
+ }
@@ -0,0 +1,25 @@
1
+ """except 收窄的具名词汇表——收窄从这里的命名集合里选,不许在调用点现编。
2
+
3
+ 把 20+ 处裸 `except Exception` 逐处收窄时产出过两类事故:元组里引用了
4
+ 未导入的名字(F821,异常发生时才求值元组 → NameError);收窄过深
5
+ (quota 侧信道放进 RuntimeError,直接打断搜索主流程)。本模块把
6
+ 「允许收窄成什么」收口成具名集合 + 一条判定规则:ruff F821 负责抓
7
+ 「名字写错」,这里的规则负责抓「语义选错」。
8
+
9
+ 判定规则——选哪种看 **try 块失败的代价**,不看它可能抛什么:
10
+
11
+ - 侧信道(失败只影响观测/记账/加速提示,不得影响主流程):
12
+ 保持 `except Exception` 不收窄,调用点加一行注释说明为什么。
13
+ - IO_BENIGN:文件/网络系统调用,失败语义是「这次没读到/没写成」。
14
+ - SHAPE_BENIGN:数据形状不符,失败语义是「这份数据不可用」。
15
+ - OPT_IMPORT:可选模块缺失,走降级路径(`except ImportError` 直接写即可,
16
+ 本集合只在与其他集合拼接时使用)。
17
+
18
+ 集合之间可以拼接(如 `IO_BENIGN + SHAPE_BENIGN`)。要收窄的异常不在任何
19
+ 集合里时,先把失败语义想清楚再改这里,不要在调用点现编一个新元组。
20
+ """
21
+
22
+ IO_BENIGN: tuple[type[Exception], ...] = (OSError,)
23
+ SHAPE_BENIGN: tuple[type[Exception], ...] = (
24
+ AttributeError, IndexError, KeyError, TypeError, ValueError)
25
+ OPT_IMPORT: tuple[type[Exception], ...] = (ImportError,)
@@ -1,30 +1,56 @@
1
1
  #!/usr/bin/env python3
2
- """extract.py — 结构化数据提取"""
2
+ """extract.py — 结构化数据提取(抓取走 fetch_v3 降级链)"""
3
3
  import json, re, sys
4
4
  from html.parser import HTMLParser
5
- from fetch import fetch_page
5
+ from cli_io import dumps, dumps_pretty
6
+ # fetch_v3:UA 轮换 + TLS 指纹 + Wayback/浏览器降级,反爬站可抓。
7
+ # 单页场景保留浏览器降级(use_browser_fallback=True 默认)。
8
+ from fetch_v3 import fetch_v3 as _fetch3
9
+
10
+
11
+ def _extract_fetch(url: str, max_chars: int, timeout: int, raw: bool = True) -> dict:
12
+ """兼容旧 fetch_page 签名:返回 {success, html, content, url, error}。
13
+
14
+ need_html=raw:结构化提取(表格 / Meta / JSON-LD)全部依赖原始 HTML,
15
+ 而 fetch_v3 默认会把 html 字段剥掉。此前这里没传,于是 `argo extract`
16
+ 恒拿不到 HTML、tables/metadata/jsonld 永远是空的——参数叫 raw 却只影响
17
+ 出参、不影响取数,正是「开关看着接上了、其实没接」的形态。
18
+ 另:need_html=True 时内容协商自动停用,该路径不会拿到 Markdown。
19
+ """
20
+ r = _fetch3(url, max_chars=max_chars, timeout=float(timeout),
21
+ need_html=raw)
22
+ out = {"url": r["url"], "content": r.get("content", ""),
23
+ "success": r["success"], "error": r.get("error", "")}
24
+ if raw:
25
+ out["html"] = r.get("html", "")
26
+ return out
6
27
 
7
28
  class TableExtractor(HTMLParser):
8
29
  def __init__(self):
9
30
  super().__init__()
10
31
  self.tables = []
11
- self._in_table = False
32
+ self._table_depth = 0
12
33
  self._rows = []
13
34
  self._row = []
14
35
  self._cell = []
15
36
  self._in_cell = False
16
37
  def handle_starttag(self, tag, attrs):
17
- if tag == 'table': self._in_table = True; self._rows = []
18
- elif tag in ('td','th') and self._in_table: self._in_cell = True; self._cell = []
19
- elif tag == 'tr' and self._in_table: self._row = []
38
+ if tag == 'table':
39
+ self._table_depth += 1
40
+ if self._table_depth == 1:
41
+ self._rows = []
42
+ elif self._table_depth == 1 and tag in ('td','th'):
43
+ self._in_cell = True; self._cell = []
44
+ elif self._table_depth == 1 and tag == 'tr':
45
+ self._row = []
20
46
  def handle_endtag(self, tag):
21
47
  if tag in ('td','th') and self._in_cell:
22
48
  self._in_cell = False; self._row.append(''.join(self._cell).strip())
23
- elif tag == 'tr' and self._in_table and self._row:
49
+ elif tag == 'tr' and self._table_depth == 1 and self._row:
24
50
  self._rows.append(self._row)
25
- elif tag == 'table' and self._in_table:
26
- self._in_table = False
27
- if self._rows:
51
+ elif tag == 'table' and self._table_depth:
52
+ self._table_depth -= 1
53
+ if self._table_depth == 0 and self._rows:
28
54
  headers = self._rows[0]
29
55
  table = [{headers[i]: row[i] if i < len(row) else '' for i in range(len(headers))} for row in self._rows[1:]]
30
56
  self.tables.append(table)
@@ -50,20 +76,33 @@ def extract_jsonld(html):
50
76
  results = []
51
77
  for m in re.finditer(r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>', html, re.S|re.I):
52
78
  try: results.append(json.loads(m.group(1)))
53
- except: pass
79
+ except Exception: pass # 非 JSON 的 ld+json 块跳过;裸 except 会连 Ctrl-C 一起吞
54
80
  return results
55
81
 
56
82
  if __name__ == '__main__':
57
83
  import argparse
58
84
  p = argparse.ArgumentParser()
59
- p.add_argument('--url', required=True)
85
+ # 位置参数与 --url 等价:bin/argo 的 usage 写的是 `argo extract "url"`,
86
+ # 而入口原先只认 --url —— 照文档敲直接报「--url required」(2026-09-15
87
+ # 实测)。保留 --url 兼容既有调用,两者都接受。
88
+ p.add_argument('target', nargs='?', metavar='URL')
89
+ p.add_argument('--url')
60
90
  p.add_argument('--mode', default='all', choices=['tables','metadata','jsonld','all'])
91
+ # 同 crawl:bin/argo 的 usage 承诺 Common flags「--json」,此前 parser 里
92
+ # 没有它,照文档传参报 unrecognized arguments。默认 3000 字符截断预览,
93
+ # --json 输出完整可解析 JSON。
94
+ p.add_argument('--json', action='store_true',
95
+ help='输出完整 JSON(默认为 3000 字符截断预览)')
61
96
  args = p.parse_args()
62
- result = fetch_page(args.url, 50000)
63
- if not result['success']: print(json.dumps({'error': result.get('error')})); sys.exit(1)
64
- html = result['content']
97
+ url = args.url or args.target
98
+ if not url:
99
+ p.error('需要 URL:argo extract <url>,或 --url <url>')
100
+ result = _extract_fetch(url, 50000, 15)
101
+ if not result['success']: print(dumps({'error': result.get('error')})); sys.exit(1)
102
+ html = result.get('html') or result.get('content') or ''
65
103
  output = {}
66
104
  if args.mode in ('tables','all'): output['tables'] = extract_tables(html)
67
105
  if args.mode in ('metadata','all'): output['metadata'] = extract_metadata(html)
68
106
  if args.mode in ('jsonld','all'): output['jsonld'] = extract_jsonld(html)
69
- print(json.dumps(output, ensure_ascii=False, indent=2)[:3000])
107
+ out = dumps(output)
108
+ print(out if args.json else out[:3000])
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ fact_align.py — 关键事实交叉标记(P0-004)
4
+
5
+ 第一性原理:
6
+ 多源检索的价值不止「更多结果」,而是「可交叉验证的事实」。
7
+ 从 title+snippet 抽取结构化事实(版本/百分比/金额/日期/法规号),
8
+ 按 (type, value) 聚合:
9
+ - 同 type 不同 value → 冲突(fact_conflicts):提示 Agent 谨慎
10
+ - 同 value ≥2 域名 → 印证(fact_corroborated):提升可信度
11
+
12
+ MECE 事实类型(互不相干、可正则化):
13
+ version 版本号 v?\\d+\\.\\d+(\\.\\d+)?
14
+ percent 百分比 \\d+(\\.\\d+)?%
15
+ money 金额 ¥/$/€ + 数字(含亿/万/billion/million 单位)
16
+ date 日期 YYYY-MM-DD / YYYY年MM月 / MM/DD/YYYY
17
+ law_code 法规号 《XX法》第N条 / 法释〔YYYY〕N号 等
18
+
19
+ 启用条件:仅 deep/auto 且结果 ≥3;fast 跳过(控制延迟)。
20
+ 输出:顶层 fact_alignment 字段。
21
+
22
+ 纯本地,仅 stdlib。
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from collections import defaultdict
29
+ from typing import Any
30
+ from urllib.parse import urlparse
31
+ from cli_io import dumps
32
+
33
+
34
+ # ── 事实抽取正则(每类捕获归一化 value)──────────────────────────────────────
35
+
36
+ # 版本号:避免把裸小数(94.9 营收)误判为版本号。
37
+ # 版本语义特征 = v 前缀 / 三段式 / 版本上下文词(version/ver/v/版本/版号)。
38
+ # 裸 "94.9" 无这些特征 → 不当版本号。
39
+ # 注意:\b 在 CJK 字符间不成立(「件版本」无单词边界),中文版本词单独处理。
40
+ _FACT_PATTERNS: dict[str, re.Pattern] = {
41
+ "version": re.compile(
42
+ r"(?i)(?:"
43
+ # 1) 英文版本词 + 数字(version 3.12 / v3.12)
44
+ r"\b(?:version|ver|release|rel)\b[\s.]*(\d+(?:\.\d+){1,3})"
45
+ r"|"
46
+ # 2) 中文版本词 + 数字(版本3.12 / 版号3.12,前导非数字避免串扰)
47
+ r"(?<![\d.])(?:版本|版号)[\s.]*(\d+(?:\.\d+){1,3})"
48
+ r"|"
49
+ # 3) 单字符 v 前缀(v3.12,独立 token)
50
+ r"(?<![a-zA-Z\d.])v[\s.]*(\d+(?:\.\d+){1,3})(?![\d.])"
51
+ r"|"
52
+ # 4) 三段式版本号(3.12.4,无需上下文词)
53
+ r"(?<![\d.])(\d+\.\d+\.\d+)(?![\d.])"
54
+ r"|"
55
+ # 5) 明确软件语义词后的双段(Python 3.12 / react 18.2)
56
+ r"\b(?:python|node|java|kubernetes|k8s|docker|react|vue|angular|"
57
+ r"tensorflow|pytorch|django|flask|ubuntu|windows|macos|ios|android)"
58
+ r"[\s.]*(\d+(?:\.\d+){1,2})"
59
+ r")", re.I),
60
+ "percent": re.compile(r"(\d+(?:\.\d+)?)\s*%|(\d+(?:\.\d+)?)\s*percent", re.I),
61
+ "money": re.compile(
62
+ r"(?:[¥$€]|人民币|美元)\s*(\d+(?:\.\d+)?(?:\s*(?:亿|万|千|百万))?)"
63
+ r"|(\d+(?:\.\d+)?)\s*(?:亿元|万元|亿美元|billion|million)", re.I),
64
+ "date": re.compile(
65
+ r"(\d{4}-\d{1,2}-\d{1,2})"
66
+ r"|(\d{4}\s*年\s*\d{1,2}\s*月(?:\s*\d{1,2}\s*日)?)"
67
+ r"|(\d{1,2}/\d{1,2}/\d{4})"),
68
+ "law_code": re.compile(
69
+ r"(《[^》]{2,20}(?:法|条例|规定|办法|解释)》(?:第[\d一二三四五六七八九十百]+条)?)"
70
+ r"|(法释〔?\d{4}〕?\d+号)"),
71
+ }
72
+
73
+
74
+ def _normalize_value(fact_type: str, raw: str) -> str:
75
+ """归一化 value,便于跨源比较。"""
76
+ v = raw.strip()
77
+ if fact_type == "percent":
78
+ return v.replace(" ", "").rstrip("%") + "%"
79
+ if fact_type == "version":
80
+ return v.lower().lstrip("v")
81
+ if fact_type == "date":
82
+ # 统一分隔符与全角
83
+ return re.sub(r"\s+", "", v).replace("年", "-").replace("月", "-").replace("日", "").strip("-")
84
+ if fact_type == "money":
85
+ return re.sub(r"\s+", "", v)
86
+ return re.sub(r"\s+", "", v)
87
+
88
+
89
+ def extract_facts(text: str) -> list[tuple[str, str]]:
90
+ """从文本抽取 (type, normalized_value) 列表。"""
91
+ facts: list[tuple[str, str]] = []
92
+ if not text:
93
+ return facts
94
+ for ftype, pat in _FACT_PATTERNS.items():
95
+ for m in pat.finditer(text):
96
+ # 取第一个非空捕获组
97
+ raw = next((g for g in m.groups() if g), None)
98
+ if not raw:
99
+ continue
100
+ val = _normalize_value(ftype, raw)
101
+ if val and (ftype, val) not in facts:
102
+ facts.append((ftype, val))
103
+ return facts
104
+
105
+
106
+ def _domain_of(url: str) -> str:
107
+ """提取 URL 的注册域名(用于跨源判定)。"""
108
+ try:
109
+ host = urlparse(url).netloc.lower()
110
+ except Exception:
111
+ return ""
112
+ if host.startswith("www."):
113
+ host = host[4:]
114
+ return host
115
+
116
+
117
+ def align_facts(results: list[dict[str, Any]], min_results: int = 3,
118
+ mode: str = "auto", depth: str = "balanced") -> dict[str, Any] | None:
119
+ """对多源结果做事实交叉标记。
120
+
121
+ Args:
122
+ results: 搜索结果列表(需含 title/snippet/url)。
123
+ min_results: 启用阈值(默认 3)。
124
+ mode: 预算模式;fast 直接跳过。
125
+ depth: 深度;fast 跳过。
126
+
127
+ Returns:
128
+ fact_alignment 字典,或 None(未启用/无事实)。
129
+ {
130
+ "enabled": True,
131
+ "fact_conflicts": [{"type","values":[{"value","domains"}]}],
132
+ "fact_corroborated": [{"type","value","domains"}],
133
+ "stats": {"facts_extracted","conflicts","corroborated"}
134
+ }
135
+ """
136
+ if mode == "fast" or depth == "fast":
137
+ return None
138
+ if not results or len(results) < min_results:
139
+ return None
140
+
141
+ # (type, value) → set(domains)
142
+ tv_domains: dict[tuple[str, str], set[str]] = defaultdict(set)
143
+ # type → set(values)
144
+ type_values: dict[str, set[str]] = defaultdict(set)
145
+
146
+ for r in results:
147
+ if not isinstance(r, dict):
148
+ continue
149
+ text = f"{r.get('title', '')} {r.get('snippet', '')}"
150
+ domain = _domain_of(r.get("url", "")) or (r.get("source", "") or "unknown")
151
+ for ftype, val in extract_facts(text):
152
+ tv_domains[(ftype, val)].add(domain)
153
+ type_values[ftype].add(val)
154
+
155
+ if not tv_domains:
156
+ return None
157
+
158
+ # 印证:同 (type,value) 出现在 ≥2 个域名
159
+ corroborated = []
160
+ for (ftype, val), domains in tv_domains.items():
161
+ if len(domains) >= 2:
162
+ corroborated.append({
163
+ "type": ftype, "value": val, "domains": sorted(domains),
164
+ })
165
+
166
+ # 冲突:同 type 出现 ≥2 个不同 value
167
+ conflicts = []
168
+ for ftype, values in type_values.items():
169
+ if len(values) >= 2:
170
+ value_entries = []
171
+ for val in sorted(values):
172
+ value_entries.append({
173
+ "value": val,
174
+ "domains": sorted(tv_domains[(ftype, val)]),
175
+ })
176
+ conflicts.append({"type": ftype, "values": value_entries})
177
+
178
+ return {
179
+ "enabled": True,
180
+ "fact_conflicts": conflicts,
181
+ "fact_corroborated": corroborated,
182
+ "stats": {
183
+ "facts_extracted": len(tv_domains),
184
+ "conflicts": len(conflicts),
185
+ "corroborated": len(corroborated),
186
+ },
187
+ }
188
+
189
+
190
+ # ── CLI 测试 ──────────────────────────────────────────────────────────────────
191
+
192
+ if __name__ == "__main__":
193
+
194
+ sample = [
195
+ {"title": "Python 3.12 发布", "snippet": "性能提升 5%,2023-10-02 正式发布",
196
+ "url": "https://python.org/news"},
197
+ {"title": "Python 3.12 新特性", "snippet": "官方称提速约 5% ,发布于 2023年10月",
198
+ "url": "https://realpython.com/py312"},
199
+ {"title": "Python 版本对比", "snippet": "3.11 相比 3.10 提速 25%",
200
+ "url": "https://blog.example.com/x"},
201
+ {"title": "营收报告", "snippet": "该公司营收 120亿元,同比增长 15%",
202
+ "url": "https://finance.a.com"},
203
+ {"title": "财报快讯", "snippet": "营收达 130亿元",
204
+ "url": "https://finance.b.com"},
205
+ ]
206
+ out = align_facts(sample, mode="auto", depth="deep")
207
+ print(dumps(out))